首页 > 解决方案 > 可空引用类型和任一模式

问题描述

我正在C# 8.0 中尝试新的 Nullable Reference Types,我正面临以下问题。

鉴于此结构:

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

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

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

解决方案


结构更新

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

where TResult : struct
where TError  : struct

当我想到 Either 模式时,我想到了 F#、模式匹配和有区别的联合,而不是 null。实际上,Either是一种避免空值的方法。事实上,问题的代码看起来像是在尝试创建一个Result 类型,而不仅仅是一个 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<>既成功又错误的方法。这可以与模式匹配一​​起使用,例如:

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

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

简化表达式

鉴于 C# 8 的属性模式,这可以重写为:

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因为 an不可能是orResult<'T,'TError>以外的东西。在 C# 中,我们还没有这个功能。OkError

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 ....


推荐阅读