C # non può rendere nullable il tipo `notnull`


9

Sto cercando di creare un tipo simile a Rust Resulto Haskell Eithere sono arrivato così lontano:

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

Dato che entrambi i parametri di tipo sono limitati notnull, perché si lamenta (ovunque ci sia un parametro di tipo con il ?segno nullable dopo di esso) che:

Un parametro di tipo nullable deve essere noto per essere un tipo di valore o un tipo di riferimento non annullabile. Prendi in considerazione l'aggiunta di un vincolo 'class', 'struct' o type.

?


Sto usando C # 8 su .NET Core 3 con tipi di riferimento nullable abilitati.


Dovresti iniziare invece dal tipo di risultato di F # e dai sindacati discriminati. Puoi facilmente ottenere qualcosa di simile in C # 8, senza portare in giro un valore morto, ma non avrai una corrispondenza esaustiva. Cercare di mettere entrambi i tipi nella stessa struttura si imbatterà in un problema dopo l'altro e riporterà gli stessi problemi che il risultato avrebbe dovuto risolvere
Panagiotis Kanavos,

Risposte:


12

Fondamentalmente stai chiedendo qualcosa che non può essere rappresentato in IL. I tipi di valore nullable e i tipi di riferimento nullable sono bestie molto diverse e, sebbene abbiano un aspetto simile nel codice sorgente, l'IL è molto diverso. La versione nullable di un tipo di valore Tè di tipo diverso ( Nullable<T>) mentre la versione nullable di un tipo di riferimento Tè dello stesso tipo, con attributi che indicano al compilatore cosa aspettarsi.

Considera questo esempio più semplice:

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

Non è valido per lo stesso motivo.

Se si vincola Ta essere una struttura, l'IL generato per il GetNullValuemetodo avrebbe un tipo di ritorno di Nullable<T>.

Se si vincola Ta essere un tipo di riferimento non annullabile, l'IL generato per il GetNullValuemetodo avrebbe un tipo restituito T, ma con un attributo per l'aspetto nullabilità.

Il compilatore non può generare IL per un metodo che ha un tipo restituito di entrambi Te Nullable<T>allo stesso tempo.

Questo è fondamentalmente tutto il risultato di tipi di riferimento nullable che non sono affatto un concetto CLR - è solo magia del compilatore per aiutarti a esprimere le intenzioni nel codice e far eseguire al compilatore un controllo in fase di compilazione.

Il messaggio di errore non è chiaro come potrebbe essere però. Tè noto per essere "un tipo di valore o un tipo di riferimento non annullabile". Un messaggio di errore più preciso (ma significativamente più wordier) sarebbe:

Un parametro di tipo nullable deve essere noto per essere un tipo di valore o essere conosciuto come un tipo di riferimento non annullabile. Prendi in considerazione l'aggiunta di un vincolo 'class', 'struct' o type.

A quel punto l'errore si applicherebbe ragionevolmente al nostro codice - il parametro type non è "noto per essere un tipo di valore" e non è "noto per essere un tipo di riferimento non annullabile". È noto per essere uno dei due, ma il compilatore deve sapere quale .


C'è anche runtime-magic: non puoi rendere nullable il nullable, anche se non c'è modo di rappresentare quella restrizione in IL. Nullable<T>è un tipo speciale che non puoi crearti. E poi c'è il punto bonus di come si fa la boxe con i tipi nulllable.
Luaan,

1
@Luaan: c'è magia di runtime per tipi di valore nullable, ma non per tipi di riferimento nullable.
Jon Skeet,

6

La ragione per l'avvertimento è spiegato nella sezione The issue with T?di provare tipi nullable di riferimento . Per farla breve, se si utilizza T?è necessario specificare se il tipo è una classe o struttura. Potresti finire per creare due tipi per ogni caso.

Il problema più profondo è che l'uso di un tipo per implementare il risultato e mantenere entrambi i valori di successo ed errore riporta gli stessi problemi che il risultato avrebbe dovuto risolvere e alcuni altri.

  • Lo stesso tipo deve contenere un valore morto, o il tipo o l'errore, o riportare valori null
  • La corrispondenza del modello sul tipo non è possibile. Dovresti usare alcune fantasiose espressioni di corrispondenza del modello posizionale per farlo funzionare.
  • Per evitare null dovrai usare qualcosa come Opzione / Forse, simile alle Opzioni di F # . Porteresti comunque un None in giro, sia per il valore che per l'errore.

Risultato (ed entrambi) in F #

Il punto di partenza dovrebbe essere il tipo di risultato di F # e i sindacati discriminati. Dopotutto, questo funziona già su .NET.

Un tipo di risultato in F # è:

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

I tipi stessi portano solo ciò di cui hanno bisogno.

I DU in F # consentono una corrispondenza esaustiva del modello senza richiedere 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

Emulazione di questo in C # 8

Sfortunatamente, C # 8 non ha ancora DU, sono programmati per C # 9. In C # 8 possiamo emularlo, ma perdiamo una corrispondenza esaustiva:

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

E usalo:

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

Senza una corrispondenza esaustiva del modello, dobbiamo aggiungere quella clausola predefinita per evitare avvisi del compilatore.

Sto ancora cercando un modo per ottenere una corrispondenza esaustiva senza introdurre valori morti, anche se sono solo un'opzione.

Opzione / Forse

La creazione di una classe Option mediante l'utilizzo di una corrispondenza esaustiva è più semplice:

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

Quale può essere usato con:

string cateGory = someValue switch { Option<Category> (_    ,false) =>"No Category",
                                     Option<Category> (var v,true)  => v.Name
                                   };
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.