Perché le variabili locali richiedono l'inizializzazione, ma i campi no?


140

Se creo un bool all'interno della mia classe, solo qualcosa del genere bool check, il valore predefinito è falso.

Quando creo lo stesso valore nel mio metodo bool check(anziché all'interno della classe), viene visualizzato l'errore "uso del controllo variabile locale non assegnato". Perché?


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Martijn Pieters

14
La domanda è vaga. "Perché la specifica lo dice" sarebbe una risposta accettabile?
Eric Lippert,

4
Perché è così che è stato fatto in Java quando lo hanno copiato. : P
Alvin Thompson,

Risposte:


177

Le risposte di Yuval e David sono sostanzialmente corrette; Riassumendo:

  • L'uso di una variabile locale non assegnata è probabilmente un bug e questo può essere rilevato dal compilatore a basso costo.
  • L'uso di un campo non assegnato o di un elemento array è meno probabile un bug ed è più difficile rilevare la condizione nel compilatore. Pertanto, il compilatore non tenta di rilevare l'uso di una variabile non inizializzata per i campi e si affida invece all'inizializzazione al valore predefinito per rendere deterministico il comportamento del programma.

Un commentatore della risposta di David chiede perché sia ​​impossibile rilevare l'uso di un campo non assegnato tramite analisi statiche; questo è il punto su cui voglio approfondire questa risposta.

Prima di tutto, per qualsiasi variabile, locale o altro, è praticamente impossibile determinare esattamente se una variabile è assegnata o non assegnata. Tener conto di:

bool x;
if (M()) x = true;
Console.WriteLine(x);

La domanda "è x assegnata?" è equivalente a "M () restituisce vero?" Supponiamo ora che M () ritorni vero se l'ultimo teorema di Fermat è vero per tutti i numeri interi inferiori a undici gajillion e falso altrimenti. Per determinare se x è definitivamente assegnato, il compilatore deve essenzialmente produrre una prova dell'ultimo teorema di Fermat. Il compilatore non è così intelligente.

Quindi quello che il compilatore fa invece per i locali è implementare un algoritmo che è veloce e sopravvaluta quando un locale non è assegnato in modo definitivo. Cioè, ha alcuni falsi positivi, dove dice "Non posso dimostrare che questo locale è assegnato" anche se tu e io lo sappiamo. Per esempio:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

Supponiamo che N () restituisca un numero intero. Tu ed io sappiamo che N () * 0 sarà 0, ma il compilatore non lo sa. (Nota: il compilatore C # 2.0 lo sapeva, ma ho rimosso tale ottimizzazione, poiché la specifica non dice che il compilatore lo sappia.)

Va bene, quindi cosa sappiamo finora? Non è pratico per la gente del posto ottenere una risposta esatta, ma possiamo sopravvalutare a buon mercato i non assegnati e ottenere un risultato abbastanza buono che gli errori sul lato di "farti correggere il tuo programma poco chiaro". Quello è buono. Perché non fare la stessa cosa per i campi? Cioè, fare un controllo di assegnazione definito che sopravvaluta a buon mercato?

Bene, in quanti modi è possibile inizializzare un locale? Può essere assegnato nel testo del metodo. Può essere assegnato all'interno di una lambda nel testo del metodo; che lambda potrebbe non essere mai invocato, quindi tali incarichi non sono rilevanti. Oppure può essere passato come "out" a un altro metodo, a quel punto possiamo supporre che sia assegnato quando il metodo ritorna normalmente. Questi sono punti molto chiari in cui è assegnato il locale e sono proprio lì con lo stesso metodo che il locale è dichiarato . Determinare l'assegnazione definita per i locali richiede solo analisi locali . I metodi tendono ad essere brevi - molto meno di un milione di righe di codice in un metodo - e quindi analizzare l'intero metodo è abbastanza veloce.

E i campi? I campi possono essere inizializzati in un costruttore ovviamente. O un inizializzatore di campo. Oppure il costruttore può chiamare un metodo di istanza che inizializza i campi. Oppure il costruttore può chiamare un metodo virtuale che initailizza i campi. Oppure il costruttore può chiamare un metodo in un'altra classe , che potrebbe trovarsi in una libreria , che inizializza i campi. I campi statici possono essere inizializzati in costruttori statici. I campi statici possono essere inizializzati da altri costruttori statici.

Essenzialmente l'inizializzatore di un campo potrebbe essere ovunque nell'intero programma , inclusi i metodi virtuali interni che verranno dichiarati nelle librerie che non sono ancora state scritte :

// Library written by BarCorp
public abstract class Bar
{
    // Derived class is responsible for initializing x.
    protected int x;
    protected abstract void InitializeX(); 
    public void M() 
    { 
       InitializeX();
       Console.WriteLine(x); 
    }
}

È un errore compilare questa libreria? Se sì, come si suppone che BarCorp risolva il bug? Assegnando un valore predefinito a x? Ma è già quello che fa il compilatore.

Supponiamo che questa libreria sia legale. Se FooCorp scrive

public class Foo : Bar
{
    protected override void InitializeX() { } 
}

è che un errore? Come dovrebbe essere capito dal compilatore? L'unico modo è eseguire un'analisi dell'intero programma che tenga traccia dell'inizializzazione statica di ogni campo su ogni possibile percorso attraverso il programma , inclusi i percorsi che implicano la scelta di metodi virtuali in fase di esecuzione . Questo problema può essere arbitrariamente difficile ; può comportare l'esecuzione simulata di milioni di percorsi di controllo. L'analisi dei flussi di controllo locali richiede microsecondi e dipende dalle dimensioni del metodo. L'analisi dei flussi di controllo globali può richiedere ore perché dipende dalla complessità di ogni metodo nel programma e di tutte le librerie .

Quindi perché non fare un'analisi più economica che non deve analizzare l'intero programma e sopravvaluta ancora più severamente? Bene, proponi un algoritmo che funzioni che non renda troppo difficile scrivere un programma corretto che effettivamente si compili, e il team di progettazione può considerarlo. Non conosco nessuno di questi algoritmi.

Ora, il commentatore suggerisce "richiede che un costruttore inizializzi tutti i campi". Non è una cattiva idea. In effetti, è un'idea non male che C # abbia già quella caratteristica per le strutture . È necessario un costruttore di strutture per assegnare definitivamente tutti i campi quando il ctor ritorna normalmente; il costruttore predefinito inizializza tutti i campi ai loro valori predefiniti.

E le lezioni? Bene, come fai a sapere che un costruttore ha inizializzato un campo ? Il ctor potrebbe chiamare un metodo virtuale per inizializzare i campi e ora siamo tornati nella stessa posizione in cui ci trovavamo prima. Le strutture non hanno classi derivate; le classi potrebbero. È necessaria una libreria contenente una classe astratta per contenere un costruttore che inizializza tutti i suoi campi? In che modo la classe astratta sa a quali valori devono essere inizializzati i campi?

John suggerisce semplicemente di vietare i metodi di chiamata in un ctor prima che i campi vengano inizializzati. Quindi, riassumendo, le nostre opzioni sono:

  • Rendere illegali gli idiomi di programmazione comuni, sicuri e usati di frequente.
  • Fai una costosa analisi dell'intero programma che richiede ore alla compilazione per cercare bug che probabilmente non ci sono.
  • Affidati all'inizializzazione automatica ai valori predefiniti.

Il team di progettazione ha scelto la terza opzione.


1
Ottima risposta, come al solito. Ho una domanda però: perché non assegnare automaticamente valori predefiniti anche alle variabili locali? In altre parole, perché non renderlo bool x;equivalente bool x = false; anche all'interno di un metodo ?
durron597,

8
@ durron597: poiché l'esperienza ha dimostrato che dimenticare di assegnare un valore a un locale è probabilmente un bug. Se è probabilmente un bug ed è economico e facile da rilevare, allora c'è un buon incentivo a rendere il comportamento illegale o un avvertimento.
Eric Lippert,

27

Quando creo lo stesso bool nel mio metodo, bool check (invece che all'interno della classe), ricevo un errore "uso del controllo variabile locale non assegnato". Perché?

Perché il compilatore sta cercando di impedirti di fare un errore.

L'inizializzazione della variabile per falsecambiare qualcosa in questo particolare percorso di esecuzione? Probabilmente no, considerando che default(bool)è falso comunque, ma ti sta costringendo a essere consapevole che ciò sta accadendo. L'ambiente .NET ti impedisce di accedere alla "garbage memory", poiché inizializzerà qualsiasi valore al loro valore predefinito. Tuttavia, immagina che questo fosse un tipo di riferimento e passeresti un valore non inizializzato (null) a un metodo in attesa di un valore non nullo e otterrai un NRE in fase di esecuzione. Il compilatore sta semplicemente cercando di impedirlo, accettando il fatto che ciò può talvolta portare a bool b = falsedichiarazioni.

Eric Lippert ne parla in un post sul blog :

Il motivo per cui vogliamo renderlo illegale non è, come molte persone credono, perché la variabile locale verrà inizializzata in immondizia e vogliamo proteggerti dall'immondizia. Infatti inizializziamo automaticamente i locali ai loro valori predefiniti. (Anche se i linguaggi di programmazione C e C ++ non lo fanno e ti permetteranno allegramente di leggere la spazzatura da un locale non inizializzato.) Piuttosto, è perché l'esistenza di un tale percorso di codice è probabilmente un bug e vogliamo buttarti nel pozzo di qualità; dovresti lavorare sodo per scrivere quel bug.

Perché questo non si applica a un campo di classe? Bene, suppongo che la linea debba essere tracciata da qualche parte, e l'inizializzazione delle variabili locali è molto più facile da diagnosticare e da correggere, al contrario dei campi di classe. Il compilatore potrebbe farlo, ma pensa a tutti i possibili controlli che dovrebbe essere fatto (dove alcuni di essi sono indipendenti dal codice della classe stessa) per valutare se ogni campo in una classe è inizializzato. Non sono un progettista di compilatori, ma sono sicuro che sarebbe sicuramente più difficile in quanto vi sono molti casi che vengono presi in considerazione e devono essere eseguiti anche in modo tempestivo . Per ogni funzione che devi progettare, scrivere, testare e distribuire e il valore dell'implementazione rispetto allo sforzo compiuto sarebbe inutile e complicato.


"immagina che questo fosse un tipo di riferimento e passeresti questo oggetto non inizializzato a un metodo aspettandone uno inizializzato" Intendevi dire: "immagina che questo fosse un tipo di riferimento e stavi passando il valore predefinito (null) invece del riferimento di un oggetto"?
Deduplicatore,

@Deduplicator Sì. Un metodo che prevede un valore non nullo. Modificato quella parte. Spero sia più chiaro ora.
Yuval Itzchakov,

Non penso che sia a causa della linea tracciata. Ogni classe suppone di avere un costruttore, almeno il costruttore predefinito. Quindi, quando rimani con il costruttore predefinito otterrai valori predefiniti (silenzioso trasparente). Quando si definisce un costruttore, ci si aspetta o si suppone che si sappia cosa si sta facendo al suo interno e quali campi si desidera vengano inizializzati in modo da includere la conoscenza dei valori predefiniti.
Peter,

Al contrario: un campo all'interno di un metodo può essere dichiarato e assegnato a valori in diversi percorsi di esecuzione. Potrebbero esserci delle eccezioni che possono essere facilmente supervisionate fino a quando non si guarda nella documentazione di un framework che è possibile utilizzare o anche in altre parti del codice che non è possibile mantenere. Ciò può introdurre un percorso di esecuzione molto complesso. Pertanto suggeriscono i compilatori.
Peter,

@Peter Non ho davvero capito il tuo secondo commento. Per quanto riguarda il primo, non è necessario inizializzare alcun campo all'interno di un costruttore. È una pratica comune . Il compito dei compilatori non è quello di applicare tale pratica. Non puoi fare affidamento su nessuna implementazione di un costruttore in esecuzione e dire "va bene, tutti i campi vanno bene". Eric ha elaborato molto nella sua risposta sui modi in cui uno può inizializzare un campo di una classe e mostra come ci vorrebbe molto tempo per calcolare l'inizializzazione di tutti i modi logici.
Yuval Itzchakov,

25

Perché le variabili locali richiedono l'inizializzazione, ma i campi no?

La risposta breve è che il codice che accede a variabili locali non inizializzate può essere rilevato dal compilatore in modo affidabile, utilizzando l'analisi statica. Considerando che questo non è il caso dei campi. Quindi il compilatore applica il primo caso, ma non il secondo.

Perché le variabili locali richiedono l'inizializzazione?

Questo non è altro che una decisione di progettazione del linguaggio C #, come spiegato da Eric Lippert . Il CLR e l'ambiente .NET non lo richiedono. VB.NET, ad esempio, verrà compilato correttamente con variabili locali non inizializzate, e in realtà il CLR inizializza tutte le variabili non inizializzate ai valori predefiniti.

Lo stesso potrebbe accadere con C #, ma i progettisti del linguaggio hanno scelto di non farlo. Il motivo è che le variabili inizializzate sono un'enorme fonte di bug e quindi, obbligando l'inizializzazione, il compilatore aiuta a ridurre gli errori accidentali.

Perché i campi non richiedono l'inizializzazione?

Quindi perché questa inizializzazione esplicita obbligatoria non avviene con i campi all'interno di una classe? Semplicemente perché quella esplicita inizializzazione potrebbe avvenire durante la costruzione, attraverso una proprietà chiamata da un inizializzatore di oggetti, o anche da un metodo chiamato molto dopo l'evento. Il compilatore non può utilizzare l'analisi statica per determinare se ogni possibile percorso attraverso il codice porta all'inizializzazione esplicita della variabile dinanzi a noi. Sbagliare sarebbe fastidioso, poiché allo sviluppatore potrebbe essere lasciato un codice valido che non verrà compilato. Quindi C # non lo applica affatto e il CLR viene lasciato per inizializzare automaticamente i campi su un valore predefinito se non impostato esplicitamente.

E i tipi di raccolta?

L'applicazione di C # dell'inizializzazione delle variabili locali è limitata, il che spesso attira gli sviluppatori. Considera le seguenti quattro righe di codice:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

La seconda riga di codice non verrà compilata, poiché sta cercando di leggere una variabile stringa non inizializzata. La quarta riga di codice si compila bene però, come arrayè stato inizializzato, ma solo con valori predefiniti. Poiché il valore predefinito di una stringa è null, otteniamo un'eccezione in fase di esecuzione. Chiunque abbia trascorso del tempo qui su Stack Overflow saprà che questa incoerenza di inizializzazione esplicita / implicita porta a molti "Perché sto ricevendo un errore" Riferimento oggetto non impostato su un'istanza di un oggetto "?" domande.


"Il compilatore non può utilizzare l'analisi statica per determinare se ogni possibile percorso attraverso il codice porta all'inizializzazione esplicita di fronte a noi." Non sono convinto che sia vero. Puoi pubblicare un esempio di un programma resistente all'analisi statica?
John Kugelman,

@JohnKugelman, considera il caso semplice public interface I1 { string str {get;set;} }e un metodo int f(I1 value) { return value.str.Length; }. Se questo esiste in una libreria, il compilatore non può sapere a cosa sarà collegata quella libreria, quindi se il settestamento sarà stato chiamato prima del get, Il campo di supporto potrebbe non essere inizializzato esplicitamente, ma deve compilare tale codice.
David Arno,

È vero, ma non mi aspetto che l'errore venga generato durante la compilazione f. Sarebbe generato durante la compilazione dei costruttori. Se lasci un costruttore con un campo possibilmente non inizializzato, sarebbe un errore. Potrebbero inoltre essere necessarie restrizioni sulla chiamata dei metodi di classe e dei getter prima che tutti i campi vengano inizializzati.
John Kugelman,

@JohnKugelman: posterò una risposta per discutere del problema che sollevi.
Eric Lippert,

4
Non è giusto. Stiamo cercando di essere in disaccordo qui!
John Kugelman,

10

Buone risposte sopra, ma ho pensato di pubblicare una risposta molto più semplice / breve per le persone pigre per leggerne una lunga (come me).

Classe

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

La proprietà Boopuò o non può essere stata inizializzata nel costruttore. Quindi, quando lo trova return Boo;, non presume che sia stato inizializzato. Sopprime semplicemente l'errore.

Funzione

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

I { }caratteri definiscono l'ambito di un blocco di codice. Il compilatore percorre i rami di questi { }blocchi tenendo traccia delle cose. Si può facilmente dire che Boonon è stato inizializzato. L'errore viene quindi attivato.

Perché esiste l'errore?

L'errore è stato introdotto per ridurre il numero di righe di codice necessarie per rendere sicuro il codice sorgente. Senza l'errore, quanto sopra sarebbe simile a questo.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

Dal manuale:

Il compilatore C # non consente l'uso di variabili non inizializzate. Se il compilatore rileva l'uso di una variabile che potrebbe non essere stata inizializzata, genera l'errore del compilatore CS0165. Per ulteriori informazioni, consultare Campi (Guida per programmatori C #). Si noti che questo errore viene generato quando il compilatore incontra un costrutto che potrebbe comportare l'uso di una variabile non assegnata, anche se il codice specifico non lo fa. Questo evita la necessità di regole troppo complesse per un incarico definito.

Riferimento: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx

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.