Alv*_*tor 5 c# generics c#-8.0 nullable-reference-types
我有一个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 可空概念与此不兼容?
是否有其他方法以类似的方式处理结果?(传递容器而不是异常)。
Ps 1
请不要建议使用result.Data!;.
Ps 2
这段代码已经用了一千行以上了,所以如果能改在界面上,而不是在用法上,那就更好了。
更新
如果您确实更改了用法并转换IsSuccess为属性,则可以摆脱可空性问题并获得详尽匹配。这个 switch 表达式是详尽的,即编译器可以检查是否满足所有可能性。它确实要求每个分支只检索一个有效的属性:
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}",
             };  
原答案
看起来真正的问题是结果模式的实现。这种模式有两个特点:
像 Rust 这样的一些语言有一个内置的类型。支持选项类型/区分联合(如 F#)的函数式语言,只需使用以下命令即可轻松实现:
type Result<'T,'TError> =
    | Ok of ResultValue:'T
    | Error of ErrorValue:'TError
详尽的模式匹配意味着客户端必须同时处理这两种情况。尽管这种类型非常普遍,但它已成为语言本身。
C# 8
在 C# 8 中,我们可以实现这两种类型,而无需进行详尽的模式匹配。目前,这些类型需要一个公共类,接口或抽象类,它实际上不需要任何成员。有很多方法可以实现它们,例如:
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 的可区分联合的语法,可以嵌套类。类型仍然可以是接口,但我真的不喜欢编写new IResult<string,string>.Fail或命名接口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值。不幸的是,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 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 更新现有类型
一些现有的函数像IsSuccess和Outcome只是方便的方法。事实上,F# 的选项类型也将值的“种类”公开为tag。我们可以将这样的方法添加到接口并从实现中返回一个固定值:
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;
    ...
}
该Description和Data性能也得以实现,作为权宜之计-他们打破了结果模式和模式匹配,使他们过时反正:
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
    ...
    public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
    ...
}
默认接口成员可用于避免乱扔具体类型:
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# 9 中,有一个MemberNotNullWhen属性可以在IsSuccess选中时隐藏相应的警告
public interface IResult<out T>
{
    [MemberNotNullWhen(true, nameof(Data))]
    bool IsSuccess();
    T? Data { get; }
}
IResult<string> res = GetSomeResult();
if(!res.IsSuccess())
   throw new Exception(); // or just return something else
var len = res.Data.Length; // no nullability warning
微软官方文档尚未更新。我在本节中收到了更多有关它的可空性信息。为了使用上述属性,您必须使用.net50,或将csproj文件中的语言版本设置为c#9。向后移植这些属性的另一种方法是使用Nullable 包。
| 归档时间: | 
 | 
| 查看次数: | 2273 次 | 
| 最近记录: |