我正在尝试创建类似于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,并且启用了可空引用类型。
答案 0 :(得分:11)
基本上,您要问一些IL中无法表示的问题。可空值类型和可空引用类型是非常不同的野兽,尽管它们在源代码中看起来相似,但IL却大不相同。值类型T
的可空版本是另一种类型(Nullable<T>
),而引用类型T
的可空版本是 same 类型,具有属性告诉编译器期望什么。
考虑以下简单示例:
public class Foo<T> where T : notnull
{
public T? GetNullValue() =>
}
出于相同原因,这是无效的。
如果我们将T
约束为一个结构,则为GetNullValue
方法生成的IL的返回类型为Nullable<T>
。
如果我们将T
约束为不可为空的引用类型,则为GetNullValue
方法生成的IL将具有T
的返回类型,但具有用于可空性方面。
编译器无法为同时具有返回类型T
和Nullable<T>
的方法生成IL。
这基本上是所有可空引用类型根本不是CLR概念的结果-这只是编译器的魔力,可以帮助您在代码中表达意图,并使编译器在编译时执行一些检查。
错误消息可能不尽如人意。已知T
是“值类型或非空引用类型”。更精确(但明显多于其他)的错误消息是:
可空类型参数必须已知为值类型,或已知为不可空引用类型。考虑添加“类”,“结构”或类型约束。
到那时,该错误将合理地应用于我们的代码-类型参数不是“已知为值类型”,也不是“已知为非空引用类型”。众所周知这是两者之一,但是编译器需要知道哪个。
答案 1 :(得分:6)
警告的原因在Try out Nullable Reference Types的The issue with T?
部分中进行了说明。长话短说,如果使用T?
,则必须指定类型是类还是结构。您可能最终会为每种情况创建两种类型。
更深层的问题是,使用一种类型来实现Result并同时保留Success和Error值会带来Result应该解决的相同问题,还有更多问题。
F#中的结果(或两者)
起点应为F#'s Result type,并应区分工会。毕竟,这已经可以在.NET上使用了。
F#中的结果类型为:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
类型本身只能满足需要。
F#中的DU允许完全模式匹配,而无需为null >
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
};