【问题标题】:Nullable Reference Types and Either Pattern可空引用类型和任一模式
【发布时间】:2019-09-24 08:33:35
【问题描述】:

我正在试用新的Nullable Reference Types in C# 8.0,但遇到了以下问题。

鉴于此结构:

public readonly struct Either<TReturn, TError>
    where TReturn : struct
    where TError : struct
{
    public TError? Error { get; }
    public TReturn? Response { get; }

    public Either(TError? error, TReturn? response)
    {
        if (error == null && response == null)
        {
           throw new ArgumentException("One argument needs not to be null.");
        }
        if (error != null && response != null)
        {
            throw new ArgumentException("One argument must be null.");
        }
        Error = error;
        Response = response;
    }
}

我如何告诉编译器 either Error Response 不为空,并且它们不能两者都为空?有没有办法用新属性做这样的事情?

【问题讨论】:

  • Either 与 NRT 无关,这不完全是 Either - 它只是一种类型而不是两种。现在最相关的功能是开关表达式和模式匹配。例如result switch { OK&lt;TReturn&gt; response=&gt;...,Error&lt;TError&gt; error=&gt;...}。您必须等待 C# 9 和可区分的联合来实现真正的 Either
  • 顺便说一句,Either 模式是什么意思?当我听到它时,我想到了 F#、模式匹配和 DI。
  • 我不知道这是否是一种模式,但更像是错误处理medium.com/techtron/functional-errors-handling-1d1b4688769d 的常见做法。我只是想知道,鉴于我们得到的奇特属性,是否有人能够说些什么:它要么将 this 作为非空返回,要么将其作为非空返回。我怀疑如果有,它可能会通过一个简单的结构来支持,比如我的问题中的结构,属性在正确的位置。
  • 你可以称之为模式或成语。这不仅仅是一种常见的做法。更好的参考是 F#'s Result type 和 Scott Wlaschin 的 Railway-Oriented programming。 Scott Wlaschin 的文章比之后的文章更好地解释了这一点
  • Is there a way to do such a thing with the new attributes ?

标签: c# nullable c#-8.0 nullable-reference-types


【解决方案1】:

结构更新

当结果类型更改为结构时,代码不会更改。要使用结构类型参数,必须在接口和类型中添加以下约束:

where TResult : struct
where TError  : struct

当我想到 Either 模式时,我想到了 F#、模式匹配和可区分联合,而不是 null。事实上,Either 是一种避免空值的方法。事实上,问题的代码看起来像是在尝试创建一个Result type,而不仅仅是一个 Either。 Scott Wlaschin 的 Railway Oriented Programming 展示了如何使用这种类型在函数式语言中实现错误处理。

在 F# 中,Result 类型定义为:

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

我们还不能在 C# 8 中这样做,因为没有可区分的联合。这些是为 C# 9 计划的。

模式匹配

我们可以做的是使用模式匹配来获得相同的行为,例如:

interface IResult<TResult,TError>{} //No need for an actual implementation

public class Success<TResult,TError>:IResult<TResult,TError>

{
    public TResult Result {get;}

    public Success(TResult result) { Result=result;}
}

public class Error<TResult,TError>:IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error) { ErrorValue=error;}
}

这样就无法创建既成功又错误的IResult&lt;&gt;。这可以与模式匹配一​​起使用,例如:

IResult<int,string> someResult=.....;

if(someResult is Success<int,string> s)
{
    //Use s.Result here
}

简化表达式

鉴于 C# 8 的 property patterns,这可以重写为:

if(someResult is Success<int,string> {Result: var result} )
{
    Console.WriteLine(result);
}

或者,使用 switch 表达式,一个典型的铁路风格调用:

IResult<int,string> DoubleIt(IResult<int,string> data)
{
    return data switch {    Error<int,string> e=>e,
                            Success<int,string> {Result: var result}=>
                                       new Success<int,string>(result*2),
                            _ => throw new Exception("Unexpected type!")
                            };
}    

F# 不需要throw,因为Result&lt;'T,'TError&gt; 不可能是OkError 以外的东西。在 C# 中,我们没有这个特性。

switch 表达式允许穷举匹配。如果默认子句也丢失,我认为编译器会生成警告。

使用解构函数

如果类型具有解构器,则表达式可以进一步简化,例如:

public class Success<TResult,TError>:IResult<TResult,TError>
{
    public TResult Result {get;}

    public Success(TResult result) { Result=result;}

    public void Deconstruct(out TResult result) { result=Result;}
}

public class Error<TResult,TError>:IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error) { ErrorValue=error;}

    public void Deconstruct(out TError error) { error=ErrorValue;}
}

在这种情况下,表达式可以写成:

return data switch {    
                Error<int,string> e => e,
                Success<int,string> (var result) => new Success<int,string>(result*3),
                _ => throw new Exception("Unexpected type!")
};

可空性

问题从可空引用类型开始,那么可空性呢?如果我们尝试传递 nulll,我们会在 C# 8 中收到警告吗?

可以,只要启用 NRT。这段代码:

#nullable enable

void Main()
{
     IResult<string,string> data=new Success<string,string>(null);
     var it=Append1(data);
     Console.WriteLine(it);
}

IResult<string,string> Append1(IResult<string,string> data)
{
    return data switch {    Error<string,string> e=>e,
                            Success<string,string> (var result)=>
                                new Success<string,string>(result+"1"),
                            _ => throw new Exception("Unexpected type!")
                            };
}

生成CS8625: Cannot convert null literal to non-nullable reference type

尝试

string? s=null;
IResult<string,string> data=new Success<string,string>(s);

生成CS8604: Possible null reference argument ....

【讨论】:

    【解决方案2】:

    在实现Either monad 时,您应该使用两个不同的构造函数。这样一来,您就可以轻松摆脱这些检查,因为您的实现将确保无法同时分配两个属性。

    public readonly class Either<TReturn, TError>
    {
        bool _successful;
        private TError _error { get; }
        private TReturn _response { get; }
    
        public Either(TError error)
        {
            _error = error;
        }
    
        public Either(TReturn response)
        {
           _successful = true;
           _response = response;
        }
    }
    

    除此之外,您需要添加一个方法(到结构中),该方法将用于从结构中提取值,并将其转换为通用返回类型:

    public Match<T>(Func<TError, T> errorFunc, Func<TResponse, T> successFunc)
        => _successful ? successFunc(_response) : errorFunc(_error);
    

    这样,您强制用户处理两种情况(成功、错误)并提供将转换为通用类型的函数:

    var errorEither = new Either<string, int>(10); // example of error code
    var successEither = new Either<string, int>("success"); // example of success
    
    var commonValueError = errorEither.Match<bool>(err => false, succ => true);
    var commonValueSuccess = successEither.Match<bool>(err => false, succ => true);
    

    【讨论】:

    • C# 已经有模式匹配,这是一个关于 C# 8 的问题。即使在 C# 7 中也不需要空值
    • 这与模式匹配无关,并且显示的实现是错误的,所以提供正确的实现不是更好 - 这将完全规避可空类型的问题吗?否则使用 E​​ither monad 没有意义...
    • Result 只不过是 Either(例如 Haskell 这么称呼它)——所以我们在谈论同样的事情。即使模式匹配与 F# 中的相同,您仍然需要遵循类型的要求。因此,为了从这种类型中提取值,您需要传递函数来处理这两种情况(使用模式匹配,或Match 方法,或其他)。这就是为什么你的实现是错误的,我希望 OP 不会遵循它。
    • 当然可以,我刚刚给你看了 :D。然而,有区别的联合或模式匹配将使实现/使用变得微不足道......这是目前 C# 的唯一区别/缺点。
    • 不知道“有用字段”的OO原理
    【解决方案3】:

    你可以用 Resharper ContractAnnotation 做这样的事情。它不是 C# 8 特定的,(但是......我认为你的示例并没有真正使用可为空的引用类型,是吗?你正在使用可为空的结构。)

    [ContractAnnotation(
       "=> pError: null, pResponse: notnull; => pError: notnull, pResponse: null"
    )]
    public void Get(out TError? pError, out TReturn? pResponse) {
       pError = Error;
       pResponse = Response;
    }
    

    (字符串的意思是=&gt;左边是输入条件,=&gt;右边是输出条件,;区分不同的情况,unlable值指的是方法返回值.所以在这种情况下:无论输入如何,输出条件都是空/非空或非空/空的以太。)

    然后使用 C# 7 功能out var

    GetAnEitherFromSomewhere()
    .Get(out var error, out var response);
    if (error != null) {
       // handle error
       return;
    }
    
    // response is now known to be not null, because we can only get here if error was null
    

    老实说,我发现 JetBrains 的注释 [NotNull][CanBeNull][ContractAnnotation] 比可空引用类型更灵活(尽管更冗长)。基本上,它们允许中间情况,其中存在值可以是null 的情况,但也有值不能是null 的情况,并且这些情况在运行时是可区分的。对于可空引用类型,我无法指定中间情况,我必须选择绝对可空或绝对不可空。

    即使是像TryParse这样的普通东西:

    // should the result be nullable with nullable reference types on?
    // you have to either lie with ! or else use ? and assume it can always be null
    public bool TryParse(string pString, out SomeClass pResult) {
       if (<cant parse>) {
          pResult = null;
          return false;
       }
    
       pResult = value;
       return true;
    }
    
    // works great with JetBrains annotations and nullable reference types off
    // now you can know that the result is null or notnull
    // based on testing the bool return value
    [ContractAnnotation("=> true, pResult: notnull; => false, pResult: null")]
    public bool TryParse(string pString, out SomeClass pResult) {
       ...
    }
    

    【讨论】:

      猜你喜欢
      • 2020-01-24
      • 2020-08-08
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      • 2020-04-22
      • 2011-03-10
      • 2012-08-01
      相关资源
      最近更新 更多