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.