C # können den Typ "notnull" nicht auf null setzen

9

Ich versuche einen Typ zu erstellen, der dem von Rust Resultoder Haskell ähnelt, Eitherund ich bin so weit gekommen:

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;
    }  
}

Angesichts der Tatsache, dass beide Typparameter beschränkt sind notnull, warum beschwert es sich (überall dort, wo ein Typparameter mit dem Nullzeichen ?dahinter steht), dass:

Ein nullfähiger Typparameter muss als Werttyp oder nicht nullbarer Referenztyp bekannt sein. Erwägen Sie das Hinzufügen einer 'Klasse', 'Struktur' oder Typeinschränkung.

?


Ich verwende C # 8 unter .NET Core 3 mit aktivierten nullfähigen Referenztypen.

Schuh Diamente
quelle
Sie sollten stattdessen vom Ergebnistyp von F # und von diskriminierten Gewerkschaften ausgehen. Sie können in C # 8 leicht etwas Ähnliches erreichen, ohne einen toten Wert herumzutragen, aber Sie werden keine erschöpfende Übereinstimmung haben. Der Versuch, beide Typen in dieselbe Struktur zu setzen, führt zu einem Problem nach dem anderen und bringt
genau

Antworten:

12

Grundsätzlich fragen Sie nach etwas, das in IL nicht dargestellt werden kann. Nullable-Werttypen und nullable-Referenztypen sind sehr unterschiedliche Bestien, und obwohl sie im Quellcode ähnlich aussehen, ist die IL sehr unterschiedlich. Die Nullable - Version eines Werttyp Tist eine andere Art ( Nullable<T>) , während die Nullable - Version eines Referenztyp Tist der gleiche Typ, mit Attributen , den Compiler zu sagen , was zu erwarten ist .

Betrachten Sie dieses einfachere Beispiel:

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

Das ist aus dem gleichen Grund ungültig.

Wenn wir uns darauf beschränken T, eine Struktur zu sein, hat die für die GetNullValueMethode generierte IL den Rückgabetyp Nullable<T>.

Wenn wir uns darauf beschränken T, ein nicht nullfähiger Referenztyp zu sein, hat die für die GetNullValueMethode generierte IL den RückgabetypT , jedoch mit einem Attribut für den Nullwertaspekt.

Der Compiler kann keine IL für eine Methode generieren, die einen Rückgabetyp von beiden Tund Nullable<T>gleichzeitig hat.

Dies ist im Grunde das Ergebnis von nullbaren Referenztypen, die überhaupt kein CLR-Konzept sind. Es ist nur Compiler-Magie, die Ihnen hilft, Absichten im Code auszudrücken und den Compiler dazu zu bringen, zur Kompilierungszeit einige Überprüfungen durchzuführen.

Die Fehlermeldung ist jedoch nicht so klar wie sie sein könnte. Tist als "Werttyp oder nicht nullbarer Referenztyp" bekannt. Eine genauere (aber deutlich wortreichere) Fehlermeldung wäre:

Ein nullfähiger Typparameter muss als Werttyp oder als nicht nullbarer Referenztyp bekannt sein. Erwägen Sie das Hinzufügen einer 'Klasse', 'Struktur' oder Typeinschränkung.

Zu diesem Zeitpunkt würde der Fehler vernünftigerweise auf unseren Code zutreffen - der Typparameter ist nicht "als Werttyp bekannt" und nicht als "nicht nullbarer Referenztyp" bekannt. Es ist bekannt, dass es einer der beiden ist, aber der Compiler muss wissen, welcher .

Jon Skeet
quelle
Es gibt auch Laufzeitmagie - Sie können eine Null nicht auf Null setzen, obwohl es keine Möglichkeit gibt, diese Einschränkung in IL darzustellen. Nullable<T>ist ein besonderer Typ, den du nicht selbst machen kannst. Und dann gibt es noch den Bonuspunkt, wie Boxen mit nulllable Typen gemacht wird.
Luaan
1
@Luaan: Es gibt Laufzeitmagie für nullbare Werttypen, aber nicht für nullbare Referenztypen.
Jon Skeet
6

Der Grund für die Warnung wird im Abschnitt The issue with T?von Try out Nullable Referenztypen . Lange Rede kurzer Sinn, wenn Sie verwendenT? , müssen Sie angeben, ob der Typ eine Klasse oder eine Struktur ist. Möglicherweise erstellen Sie für jeden Fall zwei Typen.

Das tiefere Problem besteht darin, dass die Verwendung eines Typs zum Implementieren des Ergebnisses und zum Halten der Werte für Erfolg und Fehler dieselben Probleme zurückbringt, die das Ergebnis beheben sollte, und einige weitere.

  • Der gleiche Typ muss einen toten Wert haben, entweder den Typ oder den Fehler, oder Nullen zurückbringen
  • Ein Mustervergleich für den Typ ist nicht möglich. Sie müssten einige ausgefallene Ausdrücke für den Positionsmusterabgleich verwenden, damit dies funktioniert.
  • Um zu vermeiden , nulls Sie so etwas wie eine Option verwenden / Vielleicht, ähnlich wie F # 's Optionen . Sie würden jedoch immer noch eine Keine mit sich herumtragen, entweder für den Wert oder für den Fehler.

Ergebnis (und entweder) in F #

Der Ausgangspunkt sollte der Ergebnistyp von F # und diskriminierte Gewerkschaften sein. Immerhin funktioniert das schon in .NET.

Ein Ergebnistyp in F # ist:

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

Die Typen selbst tragen nur das, was sie brauchen.

DUs in F # ermöglichen einen umfassenden Mustervergleich, ohne dass Nullen erforderlich sind:

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

Emulieren Sie dies in C # 8

Leider hat C # 8 noch keine DUs, sie sind für C # 9 geplant. In C # 8 können wir dies emulieren, aber wir verlieren die erschöpfende Übereinstimmung:

#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;
}

Und benutze es:

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()
    };
}

Ohne erschöpfenden Mustervergleich müssen wir diese Standardklausel hinzufügen, um Compiler-Warnungen zu vermeiden.

Ich bin immer noch auf der Suche nach einer Möglichkeit, eine vollständige Übereinstimmung zu erzielen, ohne tote Werte einzuführen, auch wenn diese nur eine Option sind.

Option / Vielleicht

Das Erstellen einer Optionsklasse mithilfe eines umfassenden Abgleichs ist einfacher:

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;
}

Welches kann verwendet werden mit:

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