Die Antworten von Yuval und David sind grundsätzlich richtig; zusammenfassen:
- Die Verwendung einer nicht zugewiesenen lokalen Variablen ist ein wahrscheinlicher Fehler, der vom Compiler zu geringen Kosten erkannt werden kann.
- Die Verwendung eines nicht zugewiesenen Felds oder Array-Elements ist weniger wahrscheinlich ein Fehler, und es ist schwieriger, den Zustand im Compiler zu erkennen. Daher unternimmt der Compiler keinen Versuch, die Verwendung einer nicht initialisierten Variablen für Felder zu erkennen, sondern verlässt sich stattdessen auf die Initialisierung auf den Standardwert, um das Programmverhalten deterministisch zu machen.
Ein Kommentator zu Davids Antwort fragt, warum es unmöglich ist, die Verwendung eines nicht zugewiesenen Feldes durch statische Analyse zu erkennen. Dies ist der Punkt, auf den ich in dieser Antwort näher eingehen möchte.
Zunächst ist es in der Praxis unmöglich, für eine lokale oder sonstige Variable genau zu bestimmen , ob eine Variable zugewiesen oder nicht zugewiesen ist. Erwägen:
bool x;
if (M()) x = true;
Console.WriteLine(x);
Die Frage "ist x zugewiesen?" ist äquivalent zu "Gibt M () true zurück?" Nehmen wir nun an, M () gibt true zurück, wenn Fermats letzter Satz für alle ganzen Zahlen unter elf Gajillion wahr ist, andernfalls false. Um festzustellen, ob x definitiv zugewiesen ist, muss der Compiler im Wesentlichen einen Beweis für Fermats letzten Satz liefern. Der Compiler ist nicht so schlau.
Der Compiler implementiert stattdessen für Einheimische einen Algorithmus, der schnell ist und überschätzt, wenn ein Einheimischer nicht definitiv zugewiesen ist. Das heißt, es hat einige Fehlalarme, in denen steht "Ich kann nicht beweisen, dass dieses Lokal zugewiesen ist", obwohl Sie und ich es wissen. Beispielsweise:
bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);
Angenommen, N () gibt eine Ganzzahl zurück. Sie und ich wissen, dass N () * 0 0 sein wird, aber der Compiler weiß das nicht. (Hinweis: die C # 2.0 - Compiler tat das wissen, aber ich entfernte , dass eine Optimierung, da die Spezifikation nicht sagen , dass der Compiler das weiß.)
Also gut, was wissen wir bisher? Es ist für Einheimische unpraktisch, eine genaue Antwort zu erhalten, aber wir können die Nichtzuweisung billig überschätzen und ein ziemlich gutes Ergebnis erzielen, das auf der Seite von "Lassen Sie Ihr unklares Programm reparieren" fehlerhaft ist. Das ist gut. Warum nicht dasselbe für Felder tun? Das heißt, einen eindeutigen Zuweisungsprüfer erstellen, der billig überschätzt?
Wie viele Möglichkeiten gibt es für die Initialisierung eines lokalen Objekts? Sie kann im Text der Methode zugewiesen werden. Sie kann innerhalb eines Lambdas im Text der Methode zugewiesen werden. Dieses Lambda wird möglicherweise nie aufgerufen, daher sind diese Zuweisungen nicht relevant. Oder es kann als "out" an eine andere Methode übergeben werden. An diesem Punkt können wir davon ausgehen, dass es zugewiesen wird, wenn die Methode normal zurückkehrt. Dies sind sehr klare Punkte, an denen das Lokal zugewiesen wird, und sie befinden sich genau dort in derselben Methode, mit der das Lokal deklariert wird . Die Bestimmung der endgültigen Zuordnung für Einheimische erfordert nur eine lokale Analyse . Methoden sind in der Regel kurz - weit weniger als eine Million Codezeilen in einer Methode - und daher ist die Analyse der gesamten Methode recht schnell.
Was ist nun mit Feldern? Felder können natürlich in einem Konstruktor initialisiert werden. Oder ein Feldinitialisierer. Oder der Konstruktor kann eine Instanzmethode aufrufen, die die Felder initialisiert. Oder der Konstruktor kann eine virtuelle Methode aufrufen, die die Felder initiiert. Oder der Konstruktor kann eine Methode in einer anderen Klasse aufrufen , die sich möglicherweise in einer Bibliothek befindet und die Felder initialisiert. Statische Felder können in statischen Konstruktoren initialisiert werden. Statische Felder können von anderen statischen Konstruktoren initialisiert werden .
Im Wesentlichen kann sich der Initialisierer für ein Feld an einer beliebigen Stelle im gesamten Programm befinden , einschließlich innerhalb virtueller Methoden, die in Bibliotheken deklariert werden, die noch nicht geschrieben wurden :
// 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);
}
}
Ist es ein Fehler, diese Bibliothek zu kompilieren? Wenn ja, wie soll BarCorp den Fehler beheben? Durch Zuweisen eines Standardwerts zu x? Aber genau das macht der Compiler schon.
Angenommen, diese Bibliothek ist legal. Wenn FooCorp schreibt
public class Foo : Bar
{
protected override void InitializeX() { }
}
Ist das ein Fehler? Wie soll der Compiler das herausfinden? Die einzige Möglichkeit besteht darin, eine vollständige Programmanalyse durchzuführen , die die statische Initialisierung jedes Felds auf jedem möglichen Pfad durch das Programm verfolgt , einschließlich der Pfade, bei denen zur Laufzeit virtuelle Methoden ausgewählt werden . Dieses Problem kann beliebig schwierig sein ; Es kann die simulierte Ausführung von Millionen von Steuerpfaden beinhalten. Die Analyse lokaler Kontrollflüsse dauert Mikrosekunden und hängt von der Größe der Methode ab. Die Analyse globaler Kontrollflüsse kann Stunden dauern, da dies von der Komplexität jeder Methode im Programm und aller Bibliotheken abhängt .
Warum also nicht eine billigere Analyse durchführen, bei der nicht das gesamte Programm analysiert werden muss und die nur noch stärker überschätzt wird? Schlagen Sie einen funktionierenden Algorithmus vor, der es nicht zu schwierig macht, ein korrektes Programm zu schreiben, das tatsächlich kompiliert wird, und das Designteam kann dies berücksichtigen. Ich kenne keinen solchen Algorithmus.
Nun schlägt der Kommentator vor, "dass ein Konstruktor alle Felder initialisieren muss". Das ist keine schlechte Idee. Tatsächlich ist es eine nicht schlechte Idee, dass C # diese Funktion bereits für Strukturen hat . Ein Strukturkonstruktor ist erforderlich, um alle Felder definitiv zuzuweisen, bis der ctor normal zurückkehrt. Der Standardkonstruktor initialisiert alle Felder mit ihren Standardwerten.
Was ist mit Klassen? Woher wissen Sie, dass ein Konstruktor ein Feld initialisiert hat ? Der ctor könnte eine virtuelle Methode zum Initialisieren der Felder aufrufen , und jetzt befinden wir uns wieder an der Position, an der wir uns zuvor befanden. Strukturen haben keine abgeleiteten Klassen; Klassen könnten. Ist eine Bibliothek mit einer abstrakten Klasse erforderlich, um einen Konstruktor zu enthalten, der alle seine Felder initialisiert? Woher weiß die abstrakte Klasse, mit welchen Werten die Felder initialisiert werden sollen?
John schlägt vor, das Aufrufen von Methoden in einem Ctor einfach zu verbieten, bevor die Felder initialisiert werden. Zusammenfassend sind unsere Optionen also:
- Machen Sie gebräuchliche, sichere und häufig verwendete Programmiersprachen illegal.
- Führen Sie eine teure Analyse des gesamten Programms durch, bei der die Kompilierung Stunden dauert, um nach Fehlern zu suchen, die wahrscheinlich nicht vorhanden sind.
- Verlassen Sie sich auf die automatische Initialisierung auf Standardwerte.
Das Designteam entschied sich für die dritte Option.