Warum müssen lokale Variablen initialisiert werden, Felder jedoch nicht?

140

Wenn ich in meiner Klasse einen Bool erstelle bool check, ist der Standardwert false.

Wenn ich denselben Bool in meiner Methode bool check(anstelle der Klasse) erstelle, wird die Fehlermeldung "Verwendung der nicht zugewiesenen lokalen Variablenprüfung" angezeigt. Warum?

Nachime
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Martijn Pieters
14
Die Frage ist vage. Wäre "weil die Spezifikation dies sagt" eine akzeptable Antwort?
Eric Lippert
4
Denn so wurde es in Java gemacht, als sie es kopierten. : P
Alvin Thompson

Antworten:

177

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.

Eric Lippert
quelle
1
Tolle Antwort, wie immer. Ich habe jedoch eine Frage: Warum nicht auch lokalen Variablen automatisch Standardwerte zuweisen? Mit anderen Worten, warum nicht bool x;gleich bool x = false; innerhalb einer Methode sein ?
Durron597
8
@ durron597: Weil die Erfahrung gezeigt hat, dass das Vergessen, einem lokalen Wert einen Wert zuzuweisen, wahrscheinlich ein Fehler ist. Wenn es wahrscheinlich ein Fehler ist und es billig und leicht zu erkennen ist, gibt es einen guten Anreiz, das Verhalten entweder illegal oder als Warnung zu betrachten.
Eric Lippert
27

Wenn ich das gleiche Bool in meiner Methode bool check (anstelle der Klasse) erstelle, wird die Fehlermeldung "Verwendung der nicht zugewiesenen lokalen Variablenprüfung" angezeigt. Warum?

Weil der Compiler versucht, Sie daran zu hindern, einen Fehler zu machen.

Wird Ihre Variable initialisiert, falseum etwas in diesem bestimmten Ausführungspfad zu ändern? Wahrscheinlich nicht, wenn default(bool)man bedenkt , dass es sowieso falsch ist, aber es zwingt Sie, sich dessen bewusst zu sein, dass dies geschieht. Die .NET-Umgebung verhindert, dass Sie auf "Garbage Memory" zugreifen, da alle Werte auf ihren Standardwert initialisiert werden. Stellen Sie sich dennoch vor, dies sei ein Referenztyp, und Sie würden einen nicht initialisierten (null) Wert an eine Methode übergeben, die eine Nicht-Null erwartet, und zur Laufzeit eine NRE erhalten. Der Compiler versucht lediglich, dies zu verhindern, und akzeptiert die Tatsache, dass dies manchmal zu bool b = falseAnweisungen führen kann.

Eric Lippert spricht darüber in einem Blogbeitrag :

Der Grund, warum wir dies illegal machen wollen, ist nicht, wie viele Leute glauben, weil die lokale Variable auf Müll initialisiert wird und wir Sie vor Müll schützen wollen. Tatsächlich initialisieren wir Einheimische automatisch auf ihre Standardwerte. (Obwohl die Programmiersprachen C und C ++ dies nicht tun und es Ihnen fröhlich ermöglichen, Müll von einem nicht initialisierten lokalen zu lesen.) Es liegt vielmehr daran, dass das Vorhandensein eines solchen Codepfads wahrscheinlich ein Fehler ist, und wir möchten Sie in den Code werfen Qualitätsgrube; Sie sollten hart arbeiten müssen, um diesen Fehler zu schreiben.

Warum gilt das nicht für ein Klassenfeld? Nun, ich gehe davon aus, dass die Linie irgendwo gezogen werden musste und die Initialisierung lokaler Variablen im Gegensatz zu Klassenfeldern viel einfacher zu diagnostizieren und richtig zu machen ist. Der Compiler könnte dies tun, aber denken Sie an alle möglichen Überprüfungen, die er durchführen müsste (wobei einige davon unabhängig vom Klassencode selbst sind), um zu bewerten, ob jedes Feld in einer Klasse initialisiert ist. Ich bin kein Compiler - Designer, aber ich bin sicher , dass es auf jeden Fall sein schwieriger , da es viele Fälle gibt, die berücksichtigt werden, und hat in einem getan werden rechtzeitig auch. Für jede Funktion, die Sie entwerfen, schreiben, testen und bereitstellen müssen, wäre der Wert der Implementierung im Gegensatz zum Aufwand nicht wert und kompliziert.

Yuval Itzchakov
quelle
"Stellen Sie sich vor, dies wäre ein Referenztyp, und Sie würden dieses nicht initialisierte Objekt an eine Methode übergeben, die ein initialisiertes Objekt erwartet." Meinten Sie: "Stellen Sie sich vor, dies wäre ein Referenztyp und Sie würden den Standardwert (null) anstelle der Referenz eines übergeben Objekt"?
Deduplikator
@Deduplicator Ja. Eine Methode, die einen Wert ungleich Null erwartet. Diesen Teil bearbeitet. Hoffe es ist jetzt klarer.
Yuval Itzchakov
Ich glaube nicht, dass es an der gezeichneten Linie liegt. Jede Klasse setzt einen Konstruktor voraus, zumindest den Standardkonstruktor. Wenn Sie sich also an den Standardkonstruktor halten, erhalten Sie Standardwerte (leise transparent). Wenn Sie einen Konstruktor definieren, wird von Ihnen erwartet oder erwartet, dass Sie wissen, was Sie in ihm tun und welche Felder Sie auf welche Weise initialisieren möchten, einschließlich der Kenntnis der Standardwerte.
Peter
Im Gegenteil: Ein Feld innerhalb einer Methode kann durch deklarierte und zugewiesene Werte in verschiedenen Ausführungspfaden angezeigt werden. Es kann Ausnahmen geben, die leicht zu übersehen sind, bis Sie in der Dokumentation eines Frameworks nachsehen, das Sie möglicherweise verwenden, oder sogar in anderen Teilen des Codes, die Sie möglicherweise nicht verwalten. Dies kann einen sehr komplexen Ausführungspfad einführen. Daher deuten die Compiler an.
Peter
@ Peter Ich habe deinen zweiten Kommentar nicht wirklich verstanden. In Bezug auf die erste ist es nicht erforderlich, Felder innerhalb eines Konstruktors zu initialisieren. Es ist eine übliche Praxis . Die Aufgabe des Compilers besteht nicht darin, eine solche Praxis durchzusetzen. Sie können sich nicht auf die Implementierung eines Konstruktors verlassen, der ausgeführt wird, und sagen: "In Ordnung, alle Felder sind in Ordnung". Eric erarbeitet viel in seiner Antwort auf die Art und Weise man ein Feld einer Klasse initialisieren kann, und zeigt , wie es eine dauern würde sehr lange Zeit alle logischen Möglichkeiten zur Berechnung der Initialisierung.
Yuval Itzchakov
25

Warum müssen lokale Variablen initialisiert werden, Felder jedoch nicht?

Die kurze Antwort lautet, dass Code, der auf nicht initialisierte lokale Variablen zugreift, vom Compiler mithilfe einer statischen Analyse zuverlässig erkannt werden kann. Bei Feldern ist dies nicht der Fall. Der Compiler erzwingt also den ersten Fall, aber nicht den zweiten.

Warum müssen lokale Variablen initialisiert werden?

Dies ist nicht mehr als eine Entwurfsentscheidung der C # -Sprache, wie von Eric Lippert erklärt . Die CLR und die .NET-Umgebung erfordern dies nicht. VB.NET kompiliert beispielsweise problemlos mit nicht initialisierten lokalen Variablen, und in Wirklichkeit initialisiert die CLR alle nicht initialisierten Variablen auf Standardwerte.

Das gleiche könnte mit C # passieren, aber die Sprachdesigner entschieden sich dagegen. Der Grund dafür ist, dass initialisierte Variablen eine große Fehlerquelle darstellen. Durch die Anforderung der Initialisierung hilft der Compiler, versehentliche Fehler zu reduzieren.

Warum müssen Felder nicht initialisiert werden?

Warum findet diese obligatorische explizite Initialisierung nicht bei Feldern innerhalb einer Klasse statt? Einfach, weil diese explizite Initialisierung während der Erstellung auftreten kann, indem eine Eigenschaft von einem Objektinitialisierer oder sogar von einer Methode aufgerufen wird, die lange nach dem Ereignis aufgerufen wird. Der Compiler kann keine statische Analyse verwenden, um festzustellen, ob jeder mögliche Pfad durch den Code dazu führt, dass die Variable explizit vor uns initialisiert wird. Es wäre ärgerlich, etwas falsch zu machen, da dem Entwickler möglicherweise gültiger Code übrig bleibt, der nicht kompiliert werden kann. C # erzwingt dies also überhaupt nicht und die CLR initialisiert Felder automatisch auf einen Standardwert, wenn sie nicht explizit festgelegt wird.

Was ist mit Sammlungstypen?

Die Durchsetzung der lokalen Variableninitialisierung durch C # ist begrenzt, was Entwickler häufig auffängt. Betrachten Sie die folgenden vier Codezeilen:

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

Die zweite Codezeile wird nicht kompiliert, da versucht wird, eine nicht initialisierte Zeichenfolgenvariable zu lesen. Die vierte Codezeile wird zwar wie arrayinitialisiert problemlos kompiliert , jedoch nur mit Standardwerten. Da der Standardwert einer Zeichenfolge null ist, erhalten wir zur Laufzeit eine Ausnahme. Jeder, der hier Zeit mit Stapelüberlauf verbracht hat, wird wissen, dass diese explizite / implizite Initialisierungsinkonsistenz zu sehr vielen "Warum erhalte ich den Fehler" Objektreferenz nicht auf eine Instanz eines Objekts festgelegt "führt?" Fragen.

David Arno
quelle
"Der Compiler kann keine statische Analyse verwenden, um festzustellen, ob jeder mögliche Pfad durch den Code dazu führt, dass die Variable explizit vor uns initialisiert wird." Ich bin nicht davon überzeugt, dass dies wahr ist. Können Sie ein Beispiel für ein Programm veröffentlichen, das gegen statische Analysen resistent ist?
John Kugelman
@ JohnKugelman, betrachten Sie den einfachen Fall public interface I1 { string str {get;set;} }und eine Methode int f(I1 value) { return value.str.Length; }. Wenn dies in einer Bibliothek vorhanden ist, kann der Compiler nicht wissen, womit diese Bibliothek verknüpft ist. Daher setwird der getHintergrund möglicherweise nicht explizit initialisiert, er muss jedoch solchen Code kompilieren.
David Arno
Das stimmt, aber ich würde nicht erwarten, dass der Fehler beim Kompilieren generiert wird f. Es würde beim Kompilieren der Konstruktoren generiert. Wenn Sie einen Konstruktor mit einem möglicherweise nicht initialisierten Feld belassen, ist dies ein Fehler. Möglicherweise müssen auch Einschränkungen beim Aufrufen von Klassenmethoden und Gettern vorgenommen werden, bevor alle Felder initialisiert werden.
John Kugelman
@ JohnKugelman: Ich werde eine Antwort veröffentlichen, in der das von Ihnen angesprochene Problem besprochen wird.
Eric Lippert
4
Das ist nicht fair. Wir versuchen hier eine Meinungsverschiedenheit zu haben!
John Kugelman
10

Gute Antworten oben, aber ich dachte, ich würde eine viel einfachere / kürzere Antwort veröffentlichen, damit die Leute faul sind, eine lange zu lesen (wie ich).

Klasse

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

Die Eigenschaft wurde Boomöglicherweise im Konstruktor initialisiert oder nicht . Wenn es return Boo;also gefunden wird, wird nicht davon ausgegangen, dass es initialisiert wurde. Es unterdrückt einfach den Fehler.

Funktion

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

Die { }Zeichen definieren den Umfang eines Codeblocks. Der Compiler geht durch die Zweige dieser { }Blöcke und verfolgt die Dinge. Es kann leicht festgestellt werden , dass Boonicht initialisiert wurde. Der Fehler wird dann ausgelöst.

Warum liegt der Fehler vor?

Der Fehler wurde eingeführt, um die Anzahl der Codezeilen zu verringern, die erforderlich sind, um den Quellcode sicher zu machen. Ohne den Fehler würde das obige so aussehen.

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

Aus dem Handbuch:

Der C # -Compiler erlaubt nicht die Verwendung nicht initialisierter Variablen. Wenn der Compiler die Verwendung einer Variablen erkennt, die möglicherweise nicht initialisiert wurde, generiert er den Compilerfehler CS0165. Weitere Informationen finden Sie unter Felder (C # -Programmierhandbuch). Beachten Sie, dass dieser Fehler generiert wird, wenn der Compiler auf ein Konstrukt stößt, das möglicherweise zur Verwendung einer nicht zugewiesenen Variablen führt, auch wenn Ihr bestimmter Code dies nicht tut. Dies vermeidet die Notwendigkeit zu komplexer Regeln für eine bestimmte Zuordnung.

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

Reactgular
quelle