C#和#x27;s can';不能使'notnull'类型为null

C#和#x27;s can';不能使'notnull'类型为null,c#,generics,type-constraints,c#-8.0,nullable-reference-types,C#,Generics,Type Constraints,C# 8.0,Nullable Reference Types,我正在尝试创建一个类似于Rust的结果或Haskell的的类型,我已经做到了这一点: public struct Result<TResult, TError> where TResult : notnull where TError : notnull { private readonly OneOf<TResult, TError> Value; public Result(TResult result) => Value = r

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

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;
    }  
}
公共结构结果
其中TResult:notnull
何处恐怖:非空
{
私有只读值;
公共结果(TResult Result)=>Value=Result;
公开结果(恐怖错误)=>值=错误;
公共静态隐式运算符结果(TResult结果)
=>新结果(结果);
公共静态隐式运算符结果(错误)
=>新结果(错误);
公共无效解构(out TResult?result,out TError?error)
{
结果=(Value.IsT0)?Value.AsT0:(TResult?)null;
错误=(Value.IsT1)?Value.AsT1:(恐怖?)null;
}  
}
鉴于这两种类型的参数都被限制为
notnull
,为什么它会抱怨(在任何类型参数后面有可为null的
符号的地方):

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

?



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

基本上,您要求的是无法在IL中表示的内容。可为null的值类型和可为null的引用类型是非常不同的,虽然它们在源代码中看起来相似,但IL是非常不同的。值类型
T
的可空版本是一种不同的类型(
nullable
),而引用类型
T
的可空版本是相同的类型,其属性告诉编译器需要什么

考虑这个更简单的例子:

public class Foo<T> where T : notnull
{
    public T? GetNullValue() => 
}
公共类Foo,其中T:notnull
{
公共T?GetNullValue()=>
}
出于同样的原因,这是无效的

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

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

编译器无法为返回类型同时为
t
Nullable
的方法生成IL

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

不过,错误消息并不像可能的那样清楚
T
已知为“值类型或不可为空的引用类型”。更准确(但更冗长)的错误消息是:

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


在这一点上,错误将合理地应用于我们的代码——类型参数不是“已知为值类型”,也不是“已知为不可为null的引用类型”。这是两个警告之一,但编译器需要知道是哪一个警告。

警告的原因在的
T的问题部分中解释。长话短说,如果使用
T?
则必须指定类型是类还是结构。您可能会为每个案例创建两种类型

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

  • 同一类型必须携带无效值(类型或错误),或返回空值
  • 类型上的模式匹配不可能。您必须使用一些奇特的位置模式匹配表达式来实现这一点
  • 为了避免空值,您必须使用类似Option/Maybe的东西,类似于F#。不过,无论是值还是错误,您仍然会随身携带一个None
在F#

起点应该是有歧视的工会。毕竟,这在.NET上已经起作用了

F#中的结果类型为:

在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{}​
​结构成功:IResult
{
公共TResult值{get;}
公共成功(TResult值)=>value=value;
公共void解构(out TResult value)=>value=value;
}
​结构错误:IResult
{
公共错误值{get;}
公共错误(恐怖错误)=>ErrorValue=错误;
公共无效解构(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()
    };
}
IResult Sqrt(IResult输入)
{
返回输入开关{
错误e=>e,
成功(var v)当v出现新错误(“负”),
Success(var v)=>新的Success(Math.Sqrt(v)),
_=>抛出新的ArgumentException()
};
}
如果没有详尽的模式匹配,我们必须添加那个default子句以避免编译器警告

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

选项/可能

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

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;
}
只读结构选项
{
公共只读T值{get;}
公共只读bool是一些{get;}
public readonly bool IsNone=>!IsSome;
公共期权(T值)=>(值,IsSome)=(值,true);
公共空间解构(out T值,out bool isSome)=>(value,isSome)=(value,isSome);
}
//方便的方法,类似于F#的选项模块
静态类选项
{
公共静态期权部分(T值)=>新期权(值);
公共统计
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()
    };
}
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
                                   };