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

后端 未结 2 1988
一整个雨季
一整个雨季 2020-12-11 16:01

I\'m trying to create a type similar to Rust\'s Result or Haskell\'s Either and I\'ve got this far:

public struct Result

        
2条回答
  •  孤街浪徒
    2020-12-11 16:32

    The reason for the warning is explained in the section The issue with T? of Try out Nullable Reference Types. Long story short, if you use T? you have to specify whether the type is a class or struct. You may end up creating two types for each case.

    The deeper problem is that using one type to implement Result and hold both Success and Error values brings back the same problems Result was supposed to fix, and a few more.

    • The same type has to carry a dead value around, either the type or the error, or bring back nulls
    • Pattern matching on the type isn't possible. You'd have to use some fancy positional pattern matching expressions to get this to work.
    • To avoid nulls you'll have to use something like Option/Maybe, similar to F#'s Options. You'd still carry a None around though, either for the value or error.

    Result (and Either) in F#

    The starting point should be F#'s Result type and discriminated unions. After all, this already works on .NET.

    A Result type in F# is :

    type Result<'T,'TError> =
        | Ok of ResultValue:'T
        | Error of ErrorValue:'TError
    

    The types themselves only carry what they need.

    DUs in F# allow exhaustive pattern matching without requiring nulls :

    match res2 with
    | Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
    | Error e -> printfn "Error: %s" e
    

    Emulating this in C# 8

    Unfortunately, C# 8 doesn't have DUs yet, they're scheduled for C# 9. In C# 8 we can emulate this, but we lose exhaustive matching :

    #nullable enable
    
    public interface IResult{}​
    
    ​struct Success : IResult
    {
        public TResult Value {get;}
    
        public Success(TResult value)=>Value=value;
    
        public void Deconstruct(out TResult value)=>value=Value;        
    }
    
    ​struct Error : IResult
    {
        public TError ErrorValue {get;}
    
        public Error(TError error)=>ErrorValue=error;
    
        public void Deconstruct(out TError error)=>error=ErrorValue;
    }
    

    And use it :

    IResult Sqrt(IResult input)
    {
        return input switch {
            Error e => e,
            Success (var v) when v<0 => new Error("Negative"),
            Success (var v)  => new Success(Math.Sqrt(v)),
            _ => throw new ArgumentException()
        };
    }
    

    Without exhaustive pattern matching, we have to add that default clause to avoid compiler warnings.

    I'm still looking for a way to get exhaustive matching without introducing dead values, even if they are just an Option.

    Option/Maybe

    Creating an Option class by the way that uses exhaustive matching is simpler :

    readonly struct Option 
    {
        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 Some(T value)=>new Option(value);    
        public static Option None()=>default;
    }
    

    Which can be used with :

    string cateGory = someValue switch { Option (_    ,false) =>"No Category",
                                         Option (var v,true)  => v.Name
                                       };
    

提交回复
热议问题