Perché otteniamo un possibile avviso di riferimento null di dereference, quando il riferimento null non sembra essere possibile?


9

Dopo aver letto questa domanda su HNQ, ho continuato a leggere su Nullable Reference Tipi in C # 8 e ho fatto alcuni esperimenti.

Sono consapevole che 9 volte su 10, o anche più spesso, quando qualcuno dice "Ho trovato un bug del compilatore!" questo è in realtà di progettazione e il loro malinteso. E da quando ho iniziato a esaminare questa funzione solo oggi, chiaramente non ho una buona comprensione di essa. Con questo fatto, diamo un'occhiata a questo codice:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Dopo aver letto la documentazione che ho collegato sopra, mi aspetto che la s == nullriga mi dia un avvertimento, dopo tutto sè chiaramente non annullabile, quindi confrontarla con nullnon ha senso.

Invece, sto ricevendo un avviso sulla riga successiva e l'avviso dice che sè possibile un riferimento null, anche se, per un essere umano, è ovvio che non lo è.

Inoltre, l'avviso non viene visualizzato se non confrontiamo scon null.

Ho fatto un po 'di googling e ho riscontrato un problema con GitHub , che si è rivelato riguardare qualcos'altro, ma nel frattempo ho avuto una conversazione con un collaboratore che ha fornito ulteriori informazioni su questo comportamento (ad esempio "I controlli null sono spesso un modo utile di dire al compilatore di ripristinare la sua precedente inferenza sulla nullità di una variabile. " ). Questo mi ha comunque lasciato senza risposta la domanda principale.

Piuttosto che creare un nuovo numero di GitHub e potenzialmente occupare il tempo dei partecipanti al progetto incredibilmente impegnati, lo sto diffondendo alla comunità.

Potresti spiegarmi cosa sta succedendo e perché? In particolare, perché sulla s == nulllinea non vengono generati avvisi e perché CS8602quando non sembra nullpossibile fare riferimento qui? Se l'inferenza di nullability non è a prova di proiettile, come suggerisce il thread GitHub collegato, come può andare storto? Quali sarebbero alcuni esempi di ciò?


Sembra che il compilatore stesso imposti un comportamento che a questo punto la variabile "s" potrebbe essere nulla. Ad ogni modo, se mai uso stringhe o oggetti, dovrebbe esserci sempre un controllo prima di chiamare una funzione. "s? .Length" dovrebbe fare il trucco e l'avvertimento stesso dovrebbe sparire.
chg

1
@chg, non dovrebbe essere necessario ?perché snon è nullable. Non diventa nullable, semplicemente perché eravamo abbastanza stupidi da confrontarlo null.
Andrew Savinykh,

Stavo seguendo una domanda precedente (scusate, non riesco a trovarlo) in cui è stato ipotizzato che se si aggiunge un controllo che un valore è nullo, il compilatore lo considera come 'suggerimento' che il valore potrebbe essere nullo, anche se quello non è evidentemente il caso.
stuartd

@stuartd, sì, è così che sembra. Quindi ora la domanda è: perché è utile?
Andrew Savinykh,

1
@chg, beh, è ​​quello che dico nel corpo della domanda, non è vero?
Andrew Savinykh,

Risposte:


7

Questo è effettivamente un duplicato della risposta che @stuartd ha collegato, quindi non entrerò nei dettagli super profondi qui. Ma la radice della questione è che questo non è né un bug di linguaggio né un bug del compilatore, ma il comportamento previsto è esattamente come implementato. Tracciamo lo stato null di una variabile. Quando si dichiara inizialmente la variabile, quello stato è NotNull perché lo si inizializza esplicitamente con un valore che non è null. Ma non seguiamo da dove proviene NotNull. Questo, ad esempio, è effettivamente un codice equivalente:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

In entrambi i casi, si verifica esplicitamente sper null. Prendiamo questo come input per l'analisi del flusso, proprio come Mads ha risposto a questa domanda: https://stackoverflow.com/a/59328672/2672518 . In quella risposta, il risultato è che ricevi un avviso sul reso. In questo caso, la risposta è che si riceve un avviso che indica di aver referenziato un riferimento eventualmente nullo.

Non diventa nullable, semplicemente perché eravamo abbastanza stupidi da confrontarlo null.

Sì, lo fa davvero. Al compilatore. Come umani, possiamo guardare questo codice e ovviamente capire che non può generare un'eccezione di riferimento null. Ma il modo in cui l'analisi del flusso nullable è implementata nel compilatore, non può. Abbiamo discusso di alcuni miglioramenti apportati a questa analisi in cui abbiamo aggiunto altri stati in base alla provenienza del valore, ma abbiamo deciso che ciò aggiungeva una grande complessità all'implementazione per non un grande guadagno, perché gli unici posti in cui ciò sarebbe utile in casi come questo, in cui l'utente inizializza una variabile con un newvalore o costante e quindi lo controlla nullcomunque.


Grazie. Questo riguarda principalmente le somiglianze con l'altra domanda, mi piacerebbe affrontare le differenze di più. Ad esempio, perché s == nullnon genera un avviso?
Andrew Savinykh,

Inoltre ho appena capito che con #nullable enable; string s = "";s = null;compila e funziona (produce ancora un avvertimento) quali sono i vantaggi dell'implementazione che consente di assegnare null a un "riferimento non annullabile" nel contesto di annotazione null abilitato?
Andrew Savinykh,

La risposta di Mad si concentra sul fatto che "[compilatore] non tiene traccia della relazione tra lo stato di variabili separate" in questo esempio non abbiamo variabili separate, quindi ho difficoltà ad applicare il resto della risposta di Mad a questo caso.
Andrew Savinykh,

Va da sé, ma ricordo, non sono qui per criticare, ma per imparare. Uso C # da quando è uscito per la prima volta nel 2001. Anche se non sono nuovo nel linguaggio, il modo in cui il compilatore si comporta è stato sorprendente per me. L'obiettivo di questa domanda è di appoggiare la logica per cui questo comportamento è utile per l'uomo.
Andrew Savinykh,

Ci sono validi motivi per verificare s == null. Forse, ad esempio, sei in un metodo pubblico e vuoi fare la validazione dei parametri. Oppure, forse stai usando una libreria che ha annotato in modo errato e fino a quando non risolvono quel bug devi occuparti di null dove non è stato dichiarato. In entrambi i casi, se l'avessimo avvertito, sarebbe stata una brutta esperienza. Per quanto riguarda il permesso di assegnazione: le annotazioni delle variabili locali sono solo per la lettura. Non influiscono affatto sul runtime. In effetti, inseriamo tutti questi avvisi in un unico codice di errore in modo da poterli disattivare se si desidera ridurre la variazione del codice.
333fr

0

Se l'inferenza di nullability non è a prova di proiettile, [..] come può andare storto?

Ho adottato felicemente i riferimenti nullable di C # 8 non appena erano disponibili. Dato che ero abituato a usare la notazione [NotNull] (ecc.) Di ReSharper, ho notato alcune differenze tra i due.

Il compilatore C # può essere ingannato, ma tende a errare dal lato della cautela (di solito, non sempre).

Come riferimento per i futuri visitatori, questi sono gli scenari di cui ho visto il compilatore piuttosto confuso (suppongo che tutti questi casi siano di progettazione):

  • Null perdonare null . Utilizzato spesso per evitare l'avvertenza di dereference, ma mantenendo l'oggetto non annullabile. Sembra voler tenere il piede in due scarpe.
    string s = null!; //No warning

  • Analisi di superficie . A differenza di ReSharper (che utilizza l' annotazione di codice ), il compilatore C # non supporta ancora una gamma completa di attributi per gestire i riferimenti nullable.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Tuttavia, consente di utilizzare alcuni costrutti per verificare la nullità che elimina anche l'avviso:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

o (ancora piuttosto interessante) attributi (li trovi tutti qui ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Analisi del codice sensibile . Questo è ciò che hai portato alla luce. Il compilatore deve fare ipotesi per funzionare e talvolta possono sembrare controintuitive (almeno per gli umani).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Problemi con i generici . Chiesto qui e spiegato molto bene qui (stesso articolo di prima, paragrafo "Il problema con T?"). I generici sono complicati in quanto devono rendere felici sia i riferimenti che i valori. La differenza principale è che mentre string?è solo una stringa, int?diventa a Nullable<int>e forza il compilatore a gestirli in modi sostanzialmente diversi. Anche qui, il compilatore sta scegliendo il percorso sicuro, costringendoti a specificare cosa ti aspetti:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Risolti i vincoli dando:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Ma se non utilizziamo i vincoli e rimuoviamo il '?' dai dati, siamo ancora in grado di inserire valori null in esso utilizzando la parola chiave "predefinita":

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

L'ultimo mi sembra più complicato, in quanto consente di scrivere codice non sicuro.

Spero che questo aiuti qualcuno.


Suggerirei di leggere i documenti sugli attributi nullable: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Risolveranno alcuni dei tuoi problemi, in particolare con la sezione di analisi della superficie e generici.
333fr

Grazie per il link @ 333fred. Anche se ci sono attributi con cui puoi giocare, un attributo che risolve il problema che ho pubblicato (qualcosa che mi dice che mi ThrowIfNull(s);sta assicurando che snon è nullo), non esiste. Inoltre l'articolo spiega come gestire generici non annullabili , mentre stavo mostrando come "ingannare" il compilatore, avendo un valore nullo ma nessun avvertimento al riguardo.
Alvin Sartor il

In realtà, l'attributo esiste. Ho archiviato un bug nei documenti per aggiungerlo. Stai cercando DoesNotReturnIf(bool).
333fred

@ 333fred in realtà sto cercando qualcosa di più simile DoesNotReturnIfNull(nullable).
Alvin Sartor
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.