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


9

私はラストResultやハスケルに似たタイプを作成しようとしていますが、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、なぜそれが(?その後にnull可能記号が付いた型パラメーターがあるところに)次のような不満があるのでしょうか。

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


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


代わりに、F#の結果型から始めて、ユニオンを区別する必要があります。C#8で同様の何かを簡単に達成できますが、デッドバリュー持ち歩くことはありませんが、完全なマッチングはありません。同じ構造体に両方のタイプを配置しようとすると、次々に問題が発生し、問題
そのものが返さ

回答:


12

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

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

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

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

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

Tnull可能ではない参照型になるように制約する場合、GetNullValueメソッドに対して生成されたIL の戻り型はになりますが、null可能性のT側面の属性が含まれます。

コンパイラは、両方の戻り型を有するメソッドのILを生成することができないTNullable<T>同時に。

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

エラーメッセージは、考えられるほど明確ではありません。T「値型またはnull不可の参照型」として知られています。より正確な(ただし、かなり意味のある)エラーメッセージは次のようになります。

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

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


ランタイムマジックもあります。ILでその制限を表す方法はありませんが、nullableをnullableにすることはできません。Nullable<T>自分では作れない特別なタイプです。そして、null許容型でボクシングを行う方法のボーナスポイントがあります。
Luaan

1
@Luaan:null可能値型には実行時の魔法がありますが、null可能参照型にはありません。
Jon Skeet、

6

警告の理由については、「ヌル値可能な参照型の試用」のセクションThe issue with T?で 説明しています。要するに、使用する場合、型がクラスか構造体かを指定する必要があります。ケースごとに2つのタイプを作成することになります。T?

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

  • 同じ型は、型またはエラーのいずれかでデッド値を運ぶか、またはnullを戻す必要があります
  • タイプのパターンマッチングはできません。これを機能させるには、いくつかの凝った位置パターンマッチング式を使用する必要があります。
  • nullを回避するには、F#のオプションと同様にOption / Maybeのようなものを使用する必要があります。ただし、値またはエラーのどちらでも、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
                                   };
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.