C# 不能使 `notnull` 类型可以为空

Posted

技术标签:

【中文标题】C# 不能使 `notnull` 类型可以为空【英文标题】:C#'s can't make `notnull` type nullable 【发布时间】:2020-03-10 03:29:55 【问题描述】:

我正在尝试创建类似于 Rust 的 Result 或 Haskell 的 Either 的类型,我已经做到了这一点:

public struct Result<TResult, TError>
    where TResult : notnull
    where TError : notnull

    private readonly OneOf<TResult, TError> Value;
    public Result(TResult result) => Value = result;
    public Result(TError error) => Value = error;

    public static implicit operator Result<TResult, TError>(TResult result)
        => new Result<TResult, TError>(result);

    public static implicit operator Result<TResult, TError>(TError error)
        => new Result<TResult, TError>(error);

    public void Deconstruct(out TResult? result, out TError? error)
    
        result = (Value.IsT0) ? Value.AsT0 : (TResult?)null;
        error = (Value.IsT1) ? Value.AsT1 : (TError?)null;
      

鉴于这两种类型参数都被限制为notnull,为什么它会抱怨(任何地方都有一个带有可空? 符号的类型参数):

必须知道可空类型参数是值类型或不可空引用类型。考虑添加“类”、“结构”或类型约束。

?


我在 .NET Core 3 上使用 C# 8,并启用了可为空的引用类型。

【问题讨论】:

您应该从 F#'s 结果类型开始,并区分联合。您可以在 C# 8 中轻松实现类似的功能,无需 携带死值,但您不会进行详尽的匹配。尝试将 两种 类型放在同一个结构中会遇到一个接一个的问题,并带回 Result 应该解决的问题 【参考方案1】:

基本上,您要求的是 IL 中无法表示的东西。可空值类型和可空引用类型是非常不同的野兽,虽然它们在源代码中看起来很相似,但 IL 却非常不同。值类型 T 的可空版本是不同的类型 (Nullable&lt;T&gt;),而引用类型 T 的可空版本是 same 类型,其属性告诉编译器会发生什么.

考虑这个更简单的例子:

public class Foo<T> where T : notnull

    public T? GetNullValue() => 

出于同样的原因,这是无效的。

如果我们将T 约束为一个结构,那么为GetNullValue 方法生成的IL 将具有Nullable&lt;T&gt; 的返回类型。

如果我们将T 约束为不可为空的引用类型,则为GetNullValue 方法生成的IL 将具有T 的返回类型,但具有可空性方面的属性。

编译器无法为同时具有TNullable&lt;T&gt; 的返回类型的方法生成IL。

这基本上是可空引用类型根本不是 CLR 概念的全部结果 - 它只是帮助您在代码中表达意图并让编译器在编译时执行一些检查的编译器魔法。

错误信息并不像想象的那么清晰。 T 被称为“值类型或不可为空的引用类型”。更准确(但更冗长)的错误消息是:

可空类型参数必须已知为值类型,或已知为不可空引用类型。考虑添加“类”、“结构”或类型约束。

此时错误将合理地应用于我们的代码 - 类型参数不是“已知为值类型”,也不是“已知为不可为空的引用类型”。已知它是两者之一,但编译器需要知道哪个

【讨论】:

还有运行时魔法——即使在 IL 中无法表示该限制,您也不能将可空设置设为可空。 Nullable&lt;T&gt; 是你自己做不出来的特殊类型。然后是如何使用可空类型完成装箱的好处。 @Luaan:可空值类型具有运行时魔法,但可空引用类型却没有。【参考方案2】:

在Try out Nullable Reference Types 的The issue with T? 部分中解释了警告的原因。长话短说,如果你使用T?,你必须指定类型是类还是结构。您最终可能会为每个案例创建两种类型。

更深层次的问题是,使用一种类型来实现 Result 并同时保存 Success 和 Error 值会带回 Result 应该解决的相同问题,以及更多问题。

相同的类型必须携带一个死值,无论是类型还是错误,或者带回空值 无法对类型进行模式匹配。您必须使用一些花哨的位置模式匹配表达式才能使其正常工作。 为避免空值,您必须使用 Option/Maybe 之类的东西,类似于 F# 的 Options。尽管如此,你仍然会随身携带 None ,无论是价值还是错误。

F# 中的结果(和两者之一)

起点应该是F#'s Result type 和可区分的联合。毕竟,这已经在 .NET 上运行了。

F# 中的结果类型是:

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

类型本身只携带他们需要的东西。

F# 中的 DU 允许详尽的模式匹配而不需要空值:

match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e

在 C# 8 中模拟它

不幸的是,C# 8 还没有 DU,它们被安排用于 C# 9。在 C# 8 中我们可以模拟这一点,但我们失去了详尽的匹配:

#nullable enable

public interface IResult<TResult,TError>​

​struct Success<TResult,TError> : IResult<TResult,TError>

    public TResult Value get;

    public Success(TResult value)=>Value=value;

    public void Deconstruct(out TResult value)=>value=Value;        


​struct Error<TResult,TError> : IResult<TResult,TError>

    public TError ErrorValue get;

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

    public void Deconstruct(out TError error)=>error=ErrorValue;

并使用它:

IResult<double,string> Sqrt(IResult<double,string> input)

    return input switch 
        Error<double,string> e => e,
        Success<double,string> (var v) when v<0 => new Error<double,string>("Negative"),
        Success<double,string> (var v)  => new Success<double,string>(Math.Sqrt(v)),
        _ => throw new ArgumentException()
    ;

如果没有详尽的模式匹配,我们必须添加该默认子句以避免编译器警告。

我仍在寻找一种方法来获得详尽的匹配而不引入死值,即使它们只是一个选项。

选项/可能

使用穷举匹配的方式创建一个Option类更简单:

readonly struct Option<T> 

    public readonly T Value get;

    public readonly bool IsSome get;
    public readonly bool IsNone =>!IsSome;

    public Option(T value)=>(Value,IsSome)=(value,true);    

    public void Deconstruct(out T value,out bool isSome)=>(value,isSome)=(Value,IsSome);


//Convenience methods, similar to F#'s Option module
static class Option

    public static Option<T> Some<T>(T value)=>new Option<T>(value);    
    public static Option<T> None<T>()=>default;

可用于:

string cateGory = someValue switch  Option<Category> (_    ,false) =>"No Category",
                                     Option<Category> (var v,true)  => v.Name
                                   ;

【讨论】:

以上是关于C# 不能使 `notnull` 类型可以为空的主要内容,如果未能解决你的问题,请参考以下文章

在C#中??和?分别是什么意思?

C# 8 中的不可为空的引用类型在运行时可以为空吗?

在sqlserver2000中,如果把字段改为不能为空?

C#中的??是啥意思

C#中问号的使用

约束条件