C# 8 可空值和结果容器

C# 8 nullables and Result container

我有一个 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 转换为 属性,您可以摆脱可空性问题 并且 得到穷举匹配。这个 switch 表达式是详尽无遗的,即编译器可以检查是否满足所有可能性。它确实要求每个分支只检索有效的 属性 虽然 :

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

如果你的方法接受 return 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)为此内置了类型。支持选项 types/discriminated 联合的函数式语言,如 F#,只需 :

即可轻松实现它
type Result<'T,'TError> =
    | Ok of ResultValue:'T
    | Error of ErrorValue:'TError

详尽的模式匹配意味着客户端必须处理这两种情况。尽管这种类型很常见,但它已成为语言本身。

C#8

在 C# 8 中,我们可以实现这两种类型,而无需穷举模式匹配。现在,这些类型需要一个公共 class,一个接口或抽象 class,它实际上不需要任何成员。有很多方法可以实现它们,例如:

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;
}

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

或者,要使用更接近 C# 9 的可区分联合的语法,可以嵌套 classes。类型仍然可以是接口,但我真的不喜欢写 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 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# 的选项类型还将值的 "kind" 公开为 tag 。我们可以将这样的方法添加到接口和 return 来自实现的固定值:

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");
    ...
}

可以使用默认接口成员来避免乱扔具体类型:

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);

}

修改以添加详尽匹配

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

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.0 部分(太棒了!)@PanagiotisKanavos 一篇回答:

using System;

#nullable enable

namespace ErrorHandling {
  public interface IResult<TSuccess, TError> {
    public bool OK { get; }

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

    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 class Ok<TSuccess, TError> : IResult<TSuccess, TError> {
    public bool OK => true;
    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 bool OK => false;
    public TError Error { get; }

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

  class Main {
    public IResult<int, string> F() {
      if (DateTime.Now.Year < 2020) return IResult<int, string>.Good(3);
      return IResult<int, string>.Bad("error");
    }

    public void F1() {
      var message = F() switch { 
        { OK: true, Data: var data } => $"Got some: {data}",
        { OK: false, Error: var error } => $"Oops {error}",
      };
      Console.WriteLine(message);
    }


    public void F2() {
      if (F() is { OK: false, Error: var error }) {
        Console.WriteLine(error);
        return;
      }

      if (F() is { OK: true, Data: var data }) { // Is there a way to get data without a new scope ?
        Console.WriteLine(data);
      }
    }
  }
}

在 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

官方microsoft docs暂未更新。 我在 this 章节中收到了更多关于它的可空性信息。 为了使用上述属性,您必须使用.net50,或将csproj 文件中的语言版本设置为c#9。向后移植这些属性的另一种方法是使用 Nullable package.