web-dev-qa-db-ja.com

C#は `notnull`型をnull可能にできません

私は、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に制限されているとすると、なぜそれが問題になるのですか(nullable ?後に署名)):

Null許容型パラメーターは、値型またはnull不可参照型であることがわかっている必要があります。 「クラス」、「構造」、または型制約を追加することを検討してください。


Null許容参照型を有効にして.NET Core 3でC#8を使用しています。

9
Shoe Diamente

基本的には、ILでは表現できないものを求めています。 Nullable値型とNullable参照型は非常に異なった野獣であり、ソースコードではそれらは似ていますが、ILは非常に異なっています。値型Tのnull可能バージョンは異なる型(Nullable<T>)ですが、参照型Tのnull可能バージョンはsame型ですが、コンパイラに何を期待するかを指示する属性があります。

次の簡単な例を考えてみます。

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

同じ理由でそれは無効です。

Tを構造体に制約すると、GetNullValueメソッドに対して生成されたILの戻り値の型はNullable<T>になります。

Tをnull不可の参照型に制約する場合、GetNullValueメソッドに対して生成されたILの戻り値の型はTになりますが、ヌル可能性の側面。

コンパイラーは、TNullable<T>の両方の戻り型を同時に持つメソッドのILを生成できません。

これは基本的に、null許容可能な参照型がCLRの概念ではないすべての結果です。コードで意図を表現し、コンパイル時にコンパイラーにいくつかのチェックを実行させるのは、コンパイラーの魔法です。

エラーメッセージは、考えられるほど明確ではありません。 Tは、「値型またはnullを許容しない参照型」であることがわかっています。より正確な(ただし、かなり詳細な)エラーメッセージは次のようになります。

Null可能型パラメーターは、値型であるか、null不可参照型であることがわかっている必要があります。 「クラス」、「構造体」、または型制約を追加することを検討してください。

その時点で、エラーはコードに合理的に当てはまります。typeパラメーターは「値の型であることがわかっている」ではなく、「null不可の参照型であることがわかっている」ではありません。 2つのうちの1つであることが知られていますが、コンパイラはwhichを知っている必要があります。

12
Jon Skeet

警告の理由は Nullable Reference Types のセクションThe issue with T?で説明されています。要するに、T?を使用する場合、型がクラスか構造体かを指定する必要があります。ケースごとに2つのタイプを作成することになります。

より深い問題は、1つのタイプを使用してResultを実装し、Success値とError値の両方を保持すると、Resultで修正されるはずだった同じ問題と、さらにいくつかの問題が発生することです。

  • 同じ型は、型またはエラーのいずれかでデッド値を運ぶか、nullを戻す必要があります
  • タイプのパターンマッチングはできません。これを機能させるには、いくつかの凝った位置パターンマッチング式を使用する必要があります。
  • Nullを回避するには、F#の Options のようなOption/Maybeのようなものを使用する必要があります。ただし、値とエラーのどちらでも、Noneを保持します。

F#の結果(およびいずれか)

開始点は F#の結果タイプ であり、識別された共用体である必要があります。結局のところ、これはすでに.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
                                   };
6