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


9

我正在尝试创建类似于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#的结果类型开始,并区分联合。您可以轻松实现C#8中类似的功能,而不会携带空值,但不会进行详尽的匹配。试图将两种类型放到同一结构中会导致另一个问题,并带来非常严重的问题,结果应该得到解决
Panagiotis Kanavos

Answers:


12

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

考虑以下简单示例:

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

由于相同的原因,这是无效的。

如果我们约束 T为结构,则为该GetNullValue方法生成的IL 的返回类型为Nullable<T>

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

编译器无法为返回值均为两者的方法生成IL TNullable<T>

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

错误消息不是很清楚。T已知为“值类型或非空引用类型”。更精确(但明显多了)的错误消息是:

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

到那时,错误将合理地应用于我们的代码-type参数不是“已知为值类型”,也不是“已知为非空引用类型”。众所周知这是两者之一,但是编译器需要知道which


运行时魔术也是如此-即使无法在IL中表示该限制,也无法将nullable设为nullable。Nullable<T>是一种您无法自制的特殊类型。然后还有一个好处是如何使用可空类型进行装箱。
六安

1
@Luaan:对于可为空的值类型有一个运行时魔术,但对于可为空的引用类型不是。
乔恩·斯基特

6

尝试可空引用类型部分The issue with T?中 说明了警告的原因。长话短说,如果您使用T?必须指定类型是类还是结构。您可能最终为每种情况创建两种类型。

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

  • 相同类型必须携带一个无效值(类型或错误),或者带回null
  • 类型上的模式匹配是不可能的。您必须使用一些精美的位置模式匹配表达式才能使其正常工作。
  • 为了避免空值,您必须使用Option / Maybe之类的东西,类似于F#的Options。但是,无论是值还是错误,您仍然会带一个None。

F#中的结果(或两者)

起点应该是F#的Result类型和已区分的并集。毕竟,这已经可以在.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
                                   };
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.