gpt4 book ai didi

C# 8 可空值和结果容器

转载 作者:行者123 更新时间:2023-12-03 02:13:31 25 4
gpt4 key购买 nike

我有一个IResult<T>我用来处理错误的容器。它看起来像这样:

public interface IResult<out T>
{
ResultOutcome Outcome { get; } //enum: {Failure, Uncertain, Success}
string Description { get; } //string describing the error, in case of !Success
bool IsSuccess(); //Outcome == Success
T Data { get; } //If success, it contains the data passed on, otherwise NULL
}

你可以像这样使用它:

IResult<int> GetSomething()
{
try{
int result = //things that might throw...
return Result<int>.Success(result);
}
catch(Exception e)
{
return Result<int>.Failure($"Something went wrong: {e.Message}");
}
}

然后:

var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.

int resultData = result.Data; //<- no errors, so there is something in here.

<小时/>

到目前为止,一切都很好。但是,当我引入可为空类型时,我遇到了一个问题:

public interface IResult<out T> where T : class // unfortunately this is necessary
{
...
T? Data { get; } //If success, it contains the data passed on, otherwise NULL
}
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.

int resultData = result.Data; //<- WARNING!!! POSSIBLE DEREFERENCE OF NULL

<小时/>

现在问题:我确信 result.Data包含一些东西,因为它通过了 IsSuccess()步。我怎样才能让编译器放心呢?有没有办法或者 C#8 可空概念与此不兼容?
是否有其他方法以类似的方式处理结果? (传递容器而不是异常)。

P.s. 1
请不要建议使用result.Data!;

P.s. 2
这段代码已经被使用了一千行甚至更多,所以如果改变可以在界面上,而不是在用法上,那就更好了。

最佳答案

更新

如果您确实更改了用法,并转换了IsSuccess对于属性,您可以摆脱可空性问题并获得详尽的匹配。这个开关表达式是详尽的,即编译器可以检查是否满足了所有可能性。它确实要求每个分支仅检索有效的属性:

var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};

如果您的方法接受并返回 IResult<T>对象,你可以这样写:

IResult<string> Doubler(IResult<string> input)
{
return input switch { {IsSuccess:true,Data:var data} => new Ok<string>(data+ "2"),
{IsSuccess:false} => input
};
}

...

var result2=new Ok<string>("3");
var message2=Doubler(result2) switch {
{IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
<小时/>

原始答案

看起来真正的问题是 Result 的实现图案。这种模式有两个特点:

  • 它可以防止在类型级别使用无效结果值。它通过使用两种不同的类型来表示好的结果和坏的结果来实现这一点。通过这样做,每种类型仅携带其所需的内容。
  • 它强制客户端处理所有情况或明确忽略它们。

像 Rust 这样的一些语言有一个内置类型。支持选项类型/可区分联合(如 F#)的函数式语言只需以下操作即可轻松实现:

type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError

详尽的模式匹配意味着客户端必须处理这两种情况。不过这种类型很常见,它已经融入了语言本身。

C# 8

在 C# 8 中,我们可以实现这两种类型,而无需进行详尽的模式匹配。目前,这些类型需要一个公共(public)类,无论是接口(interface)还是抽象类,它实际上不需要任何成员。有很多方法可以实现它们,例如:

public interface IResult<TSuccess,TError>{}

public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
public TSuccess Data{get;}

public Ok(TSuccess data)=>Data=data;

public void Deconstruct(out TSuccess data)=>data=Data;
}

public class Fail<TSuccess,TError>:IResult<TSuccess,TError>
{
public TError Error{get;}

public Fail(TError error)=>Error=error;

public void Deconstruct(out TError error)=>error=Error;
}

我们可以使用结构而不是类。

或者,要使用更接近 C# 9 的可区分联合的语法,可以嵌套类。类型仍然可以是接口(interface),但是我真的不喜欢写 new IResult<string,string>.Fail或命名接口(interface)Result而不是IResult :

public abstract class Result<TSuccess,TError>
{
public class Ok:Result<TSuccess,TError>
{
public TSuccess Data{get;}
public Ok(TSuccess data)=>Data=data;
public void Deconstruct(out TSuccess data)=>data=Data;
}

public class Fail:Result<TSuccess,TError>
{
public TError Error{get;}
public Fail(TError error)=>Error=error;
public void Deconstruct(out TError error)=>error=Error;
}

//Convenience methods
public static Result<TSuccess,TError> Good(TSuccess data)=>new Ok(data);
public static Result<TSuccess,TError> Bad(TError error)=>new Fail(error);
}

我们可以使用模式匹配来处理 Result值(value)观。不幸的是,C# 8 不提供详尽的匹配,因此我们还需要添加默认情况。

var result=Result<string,string>.Bad("moo");
var message=result switch { Result<string,string>.Ok (var Data) => $"Got some: {Data}",
Result<string,string>.Fail (var Error) => $"Oops {Error}"
_ => throw new InvalidOperationException("Unexpected result case")
};

C# 9

C# 9(可能)将通过 enum classes 添加可区分联合。我们将能够写:

enum class Result
{
Ok(MySuccess Data),
Fail(MyError Error)
}

并通过模式匹配来使用它。只要有匹配的解构函数,此语法就已经可以在 C# 8 中使用。 C# 9 将添加详尽的匹配,并且可能还会简化语法:

var message=result switch { Result.Ok (var Data) => $"Got some: {Data}",
Result.Fail (var Error) => $"Oops {Error}"
};

通过 DIM 更新现有类型

一些现有的函数,例如 IsSuccessOutcome只是方便的方法。事实上,F# 的选项类型还将值的“种类”公开为标签。我们可以将此类方法添加到接口(interface)中并从实现中返回固定值:

public interface IResult<TSuccess,TError>
{
public bool IsSuccess {get;}
public bool IsFailure {get;}
public bool ResultOutcome {get;}
}

public class Ok<TSuccess,string>:IResult<TSuccess,TError>
{
public bool IsSuccess =>true;
public bool IsFailure =>false;
public bool ResultOutcome =>ResultOutcome.Success;
...
}

DescriptionData属性也可以作为权宜之计来实现 - 它们破坏了结果模式,而模式匹配无论如何都会使它们过时:

public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
...
public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
...
}

可以使用默认接口(interface)成员来避免乱七八糟的具体类型:

public interface IResult<TSuccess,TError>
{
//Migration methods
public TSuccess Data=>
(this is Ok<TSuccess,TError> (var Data))
?Data
:throw new InvalidOperationException("An Error has no data");

public TError Description=>
(this is Fail<TSuccess,TError> (var Error))
?Error
:throw new InvalidOperationException("A Success Result has no Description");

//Convenience methods
public static IResult<TSuccess,TError> Good(TSuccess data)=>new Ok<TSuccess,TError>(data);
public static IResult<TSuccess,TError> Bad(TError error)=>new Fail<TSuccess,TError>(error);

}

修改以添加详尽匹配

如果我们只使用一个标志和迁移属性,我们就可以避免模式匹配异常中的默认情况:

public interface IResult<TSuccess,TError>
{
public bool IsSuccess{get;}
public bool IsFailure=>!IsSuccess;
//Migration methods
...
}

var message2=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};

这一次,编译器检测到只有两种情况,并且都被覆盖。迁移属性允许编译器检索正确的类型。使用的代码必须更改使用正确的模式,但我怀疑它已经有效了

关于C# 8 可空值和结果容器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59702550/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com