Riferimenti non annullabili C # 8 e modello Try


23

Esiste un modello nelle classi C # esemplificato da Dictionary.TryGetValuee int.TryParse: un metodo che restituisce un valore booleano che indica il successo di un'operazione e un parametro out contenente il risultato effettivo; se l'operazione non riesce, il parametro out è impostato su null.

Supponiamo che io stia usando riferimenti non annullabili C # 8 e voglia scrivere un metodo TryParse per la mia classe. La firma corretta è questa:

public static bool TryParse(string s, out MyClass? result);

Poiché il risultato è nullo nel caso falso, la variabile out deve essere contrassegnata come nullable.

Tuttavia, il modello Try viene generalmente utilizzato in questo modo:

if (MyClass.TryParse(s, out var result))
{
  // use result here
}

Poiché inserisco il ramo solo quando l'operazione ha esito positivo, il risultato non dovrebbe mai essere nullo in quel ramo. Ma poiché l'ho contrassegnato come nullable, ora devo verificarlo o utilizzare !per sovrascrivere:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
  Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}

Questo è brutto e un po 'non ergonomico.

A causa del tipico modello di utilizzo, ho un'altra opzione: mentire sul tipo di risultato:

public static bool TryParse(string s, out MyClass result) // not nullable
{
   // Happy path sets result to non-null and returns true.
   // Error path does this:
   result = null!; // override compiler complaint
   return false;
}

Ora l'uso tipico diventa più bello:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}

ma l'uso atipico non riceve l'avviso che dovrebbe:

else
{
  Console.WriteLine("Fail: {0}", result.SomeProperty);
  // Yes, result is in scope here. No, it will never be non-null.
  // Yes, it will throw. No, the compiler won't warn about it.
}

Ora non sono sicuro su quale strada andare qui. Esiste una raccomandazione ufficiale del team linguistico C #? Esiste già un codice CoreFX già convertito in riferimenti non annullabili che potrebbe mostrarmi come fare? (Sono andato alla ricerca di TryParsemetodi. IPAddressÈ una classe che ne ha una, ma non è stata convertita nel ramo principale di corefx.)

E come fa il codice generico a Dictionary.TryGetValuegestire questo? (Forse con un MaybeNullattributo speciale da quello che ho trovato.) Cosa succede quando ho un'istanza di un Dictionarytipo di valore non annullabile?


Non l'ho provato (motivo per cui non sto scrivendo questo come una risposta), ma con la nuova funzione di corrispondenza dei pattern delle istruzioni switch, suppongo che un'opzione sia semplicemente restituire il riferimento nullable (no Try-pattern, ritorno MyClass?), e fai un interruttore su di esso con un case MyClass myObj:e (un'opzione) case null:.
Filip Milovanović,

+1 Mi piace molto questa domanda, e quando ho dovuto lavorare con questo ho sempre usato un controllo null aggiuntivo anziché l'override - che si sentiva sempre un po 'inutile e inelegante, ma non è mai stato in codice critico per le prestazioni quindi l'ho lasciato andare. Sarebbe bello vedere se esiste un modo più pulito di gestirlo!
BrianH,

Risposte:


10

Il modello bool / out-var non funziona bene con tipi di riferimento nullable, come descrivi. Quindi, piuttosto che combattere il compilatore, usa la funzione per semplificare le cose. Aggiungete le funzionalità di corrispondenza dei modelli migliorate di C # 8 e potete considerare un riferimento nullable come un "tipo forse povero":

public static MyClass? TryParse(string s) => 



if (TryParse(someString) is {} myClass)
{
    // myClass wasn't null, we are good to use it
}

In questo modo, eviti di fare confusione con i outparametri e non devi lottare con il compilatore per mescolare nullcon riferimenti non annullabili.

E come fa il codice generico a Dictionary.TryGetValuegestire questo?

A questo punto, quel "povero forse è un tipo" cade. La sfida che dovrai affrontare è che quando si utilizzano i tipi di riferimento nullable (NRT), il compilatore verrà considerato Foo<T>non nullable. Ma prova a cambiarlo in Foo<T?>e vorrà che sia Tvincolato a una classe o struttura poiché i tipi di valore nullable sono una cosa molto diversa dal punto di vista del CLR. Ci sono una varietà di soluzioni a questo:

  1. Non abilitare la funzione NRT,
  2. Inizia a utilizzare default(insieme a !) per i outparametri anche se il tuo codice si sta registrando senza null,
  3. Usa un Maybe<T>tipo reale come valore di ritorno, che non è mai nulle quindi lo avvolge boole out Tin HasValuee le Valueproprietà o alcuni di questi,
  4. Usa una tupla:
public static (bool success, T result) TryParse<T>(string s) => 


if (TryParse<MyClass>(someString) is (true, var result))
{
    // result is valid here, as success is true
}

Personalmente preferisco usare, Maybe<T>ma avere un supporto per decostruire in modo che possa essere abbinato come una tupla come in 4, sopra.


2
TryParse(someString) is {} myClass- Questa sintassi richiederà un po 'di tempo per abituarsi, ma mi piace l'idea.
Sebastian Redl,

TryParse(someString) is var myClassmi sembra più facile.
Olivier Jacot-Descombes,

2
@ OlivierJacot-Descombes Può sembrare più facile ... ma non funzionerà. Il modello di auto corrisponde sempre, quindi x is var ysarà sempre vero, che xsia nullo o no.
David Arno,

15

Se arrivate a questo un po 'tardi, come me, si scopre che il team .NET lo ha affrontato attraverso un mucchio di attributi di parametro come MaybeNullWhen(returnValue: true)nello System.Diagnostics.CodeAnalysisspazio che è possibile utilizzare per il modello di prova.

Per esempio:

come gestisce questo codice generico come Dictionary.TryGetValue?

bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);

il che significa che vieni urlato se non controlli per a true

// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
  var more = result * 42;
}

// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"

Maggiori dettagli:


3

Non penso che ci sia un conflitto qui.

la tua obiezione a

public static bool TryParse(string s, out MyClass? result);

è

Poiché inserisco il ramo solo quando l'operazione ha esito positivo, il risultato non dovrebbe mai essere nullo in quel ramo.

Tuttavia, in realtà non c'è nulla che impedisca l'assegnazione di null al parametro out nelle funzioni TryParse di vecchio stile.

per esempio.

MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true

L'avvertimento dato al programmatore quando usano il parametro out senza controllo è corretto. Dovresti controllare!

Ci saranno molti casi in cui sarai costretto a restituire tipi nullable in cui il ramo principale del codice restituisce un tipo non nullable. L'avvertimento è proprio lì per aiutarti a renderli espliciti. vale a dire.

MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);

Il modo non annullabile di codificare genererà un'eccezione in cui ci sarebbe stato un valore nullo. Se stai analizzando, ottenendo o iniziando

MyClass x = (new List<MyClass>()).First(i=>i==1);

Non credo che FirstOrDefaultpossa essere confrontato, perché la nullità del suo valore di ritorno è il segnale principale. Nei TryParsemetodi, il parametro out non essendo nullo se il valore restituito è true fa parte del contratto del metodo.
Sebastian Redl,

non fa parte del contratto. l'unica cosa certa è che qualcosa è assegnato al parametro out
Ewan,

È il comportamento che mi aspetto da un TryParsemetodo. Se IPAddress.TryParsemai restituito true ma non assegnasse un valore nullo al parametro out, lo segnalerei come un bug.
Sebastian Redl,

Le tue aspettative sono comprensibili ma non vengono applicate dal compilatore. Quindi, le specifiche per IpAddress potrebbero dire che non restituisce mai true e null, ma il mio esempio JsonObject mostra un caso in cui il ritorno null potrebbe essere corretto
Ewan,

"Le tue aspettative sono comprensibili ma non sono imposte dal compilatore." - Lo so, ma la mia domanda è come scrivere al meglio il mio codice per esprimere il contratto che ho in mente, non quale sia il contratto di qualche altro codice.
Sebastian Redl,
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.