C#和#x27;s can';不能使'notnull'类型为null
我正在尝试创建一个类似于Rust的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
结果
或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
};