Posso fare un'ipotesi ragionevole su cosa sta succedendo qui, ma è tutto un po 'complicato :) Comprende lo stato e il tracciamento null descritti nella bozza delle specifiche . Fondamentalmente, nel punto in cui vogliamo tornare, il compilatore avviserà se lo stato dell'espressione è "forse null" anziché "non null".
Questa risposta è in qualche modo narrativa piuttosto che solo "ecco le conclusioni" ... Spero sia più utile in questo modo.
Semplificherò leggermente l'esempio eliminando i campi e considererò un metodo con una di queste due firme:
public static string M(string? text)
public static string M(string text)
Nelle implementazioni di seguito ho assegnato a ciascun metodo un numero diverso in modo da poter fare riferimento a esempi specifici senza ambiguità. Inoltre, consente a tutte le implementazioni di essere presenti nello stesso programma.
In ciascuno dei casi descritti di seguito, faremo varie cose, ma alla fine cercheremo di tornare text
, quindi è lo stato nullo di text
ciò che è importante.
Ritorno incondizionato
Innanzitutto, proviamo a restituirlo direttamente:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
Finora così semplice. Lo stato nullable del parametro all'inizio del metodo è "forse null" se è di tipo string?
e "non null" se è di tipo string
.
Ritorno condizionale semplice
Ora controlliamo per null all'interno della if
condizione dell'istruzione stessa. (Vorrei usare l'operatore condizionale, che credo abbia lo stesso effetto, ma volevo rimanere più fedele alla domanda.)
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
Ottimo, quindi sembra all'interno di if
un'istruzione in cui la condizione stessa verifica la nullità, lo stato della variabile all'interno di ciascun ramo if
dell'istruzione può essere diverso: all'interno del else
blocco, lo stato è "non nullo" in entrambi i pezzi di codice. Quindi, in particolare, in M3 lo stato cambia da "forse null" a "non null".
Ritorno condizionale con una variabile locale
Ora proviamo a sollevare quella condizione su una variabile locale:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
Sia M5 che M6 emettono avvisi. Quindi non solo non otteniamo l'effetto positivo del cambio di stato da "forse null" a "non null" in M5 (come abbiamo fatto in M3) ... otteniamo l' effetto opposto in M6, da dove lo stato passa " non null "a" forse null ". Mi ha davvero sorpreso.
Quindi sembra che abbiamo imparato che:
- La logica "come è stata calcolata una variabile locale" non viene utilizzata per propagare le informazioni sullo stato. Ne parleremo più avanti.
- L'introduzione di un confronto null può avvisare il compilatore che qualcosa che in precedenza pensava non fosse null potrebbe essere nullo dopo tutto.
Ritorno incondizionato dopo un confronto ignorato
Diamo un'occhiata al secondo di quei punti elenco, introducendo un confronto prima di un ritorno incondizionato. (Quindi ignoriamo completamente il risultato del confronto.):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
Nota come M8 dovrebbe essere equivalente a M2 - entrambi hanno un parametro non nullo che restituiscono incondizionatamente - ma l'introduzione di un confronto con null cambia lo stato da "non null" a "forse null". Possiamo ottenere ulteriori prove di ciò provando a dereference text
prima della condizione:
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
Nota come l' return
istruzione non abbia ora un avviso: lo stato dopo l' esecuzione text.Length
è "non nullo" (perché se eseguiamo quell'espressione con successo, non potrebbe essere nulla). Quindi il text
parametro inizia come "non null" a causa del suo tipo, diventa "forse null" a causa del confronto null, quindi diventa di nuovo "non null" dopo text2.Length
.
Quali confronti influenzano lo stato?
Quindi questo è un confronto di text is null
... quali effetti hanno comparazioni simili? Ecco altri quattro metodi, tutti che iniziano con un parametro stringa non nullable:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
Quindi, anche se x is object
ora è un'alternativa consigliata a x != null
, non hanno lo stesso effetto: solo un confronto con null (con uno qualsiasi di is
, ==
o !=
) cambia lo stato da "non null" a "forse null".
Perché il sollevamento della condizione ha un effetto?
Tornando al nostro primo punto elenco in precedenza, perché M5 e M6 non tengono conto della condizione che ha portato alla variabile locale? Questo non mi sorprende tanto quanto sembra sorprendere gli altri. Costruire quel tipo di logica nel compilatore e nelle specifiche richiede molto lavoro e per un beneficio relativamente scarso. Ecco un altro esempio che non ha nulla a che fare con l'annullamento in cui l'inserimento di qualcosa ha un effetto:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
Anche se noi sappiamo che alwaysTrue
sarà sempre vero, non soddisfa i requisiti del disciplinare che rendono il codice dopo la if
dichiarazione irraggiungibile, che è quello che ci serve.
Ecco un altro esempio di incarico definito:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
Anche se noi sappiamo che il codice entrerà esattamente uno di quei if
corpi dichiarazione, non c'è nulla nelle specifiche di lavoro che fuori. Gli strumenti di analisi statica potrebbero essere in grado di farlo, ma cercare di inserirlo nella specifica del linguaggio sarebbe una cattiva idea, IMO: va bene che gli strumenti di analisi statica abbiano tutti i tipi di euristica che possono evolversi nel tempo, ma non così tanto per una specifica della lingua.