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

C#'s can't make `notnull` type nullable

我正在尝试创建类似于 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,为什么它会抱怨(在类型参数后面带有可空 ? 符号的任何地方):

A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.

?


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

基本上,您要求的是无法在 IL 中表示的内容。可空值类型和可空引用类型是截然不同的野兽,虽然它们在源代码中看起来很相似,但 IL 却非常不同。值类型的可空版本 T 是不同的类型 (Nullable<T>),而引用类型的可空版本 T 相同的 类型, 属性告诉编译器期望什么。

考虑这个更简单的例子:

public class Foo<T> where T : notnull
{
    public T? GetNullValue() => 
}

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

如果我们将 T 约束为一个结构,那么为 GetNullValue 方法生成的 IL 将具有 return 类型的 Nullable<T>

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

编译器无法为同时具有 TNullable<T> 类型的 return 方法生成 IL。

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

错误消息并不像它可能的那样清楚。 T 已知为 "a value type or non-nullable reference type"。更精确(但更冗长)的错误消息是:

A nullable type parameter must be known to be a value type, or known to be a non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.

此时错误将合理地应用于我们的代码 - 类型参数不是 "known to be a value type" 也不是 "known to be a non-nullable reference type"。已知它是两者之一,但编译器需要知道哪个

警告的原因在 Try out Nullable Reference TypesThe issue with T? 部分进行了解释。长话短说,如果您使用 T?,则必须指定类型是 class 还是结构。您最终可能会为每种情况创建两种类型。

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

  • 同一类型必须携带死值,类型或错误,或者带回空值
  • 无法对类型进行模式匹配。您必须使用一些花哨的位置模式匹配表达式才能使其正常工作。
  • 为了避免空值,您必须使用类似于 F# 的 Options 的 Option/Maybe。不过,您仍会携带 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/Maybe

通过使用穷举匹配的方式创建选项 class 更简单:

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