Klassenfelder im Konstruktor oder bei der Deklaration initialisieren?

413

Ich habe kürzlich in C # und Java programmiert und bin gespannt, wo ich meine Klassenfelder am besten initialisieren kann.

Soll ich es bei der Erklärung tun?:

public class Dice
{
    private int topFace = 1;
    private Random myRand = new Random();

    public void Roll()
    {
       // ......
    }
}

oder in einem Konstruktor?:

public class Dice
{
    private int topFace;
    private Random myRand;

    public Dice()
    {
        topFace = 1;
        myRand = new Random();
    }

    public void Roll()
    {
        // .....
    }
}

Ich bin wirklich neugierig, was einige von euch Veteranen für die beste Praxis halten. Ich möchte konsequent sein und mich an einen Ansatz halten.

mmcdole
quelle
3
Beachten Sie, dass Sie für Strukturen keine Instanzfeldinitialisierer haben können, sodass Sie keine andere Wahl haben, als den Konstruktor zu verwenden.
Jojo

Antworten:

310

Meine Regeln:

  1. Nicht initialisiert werden mit den Standardwerten in Deklaration ( null, false, 0, 0.0...).
  2. Bevorzugen Sie die Initialisierung in der Deklaration, wenn Sie keinen Konstruktorparameter haben, der den Wert des Felds ändert.
  3. Wenn sich der Wert des Felds aufgrund eines Konstruktorparameters ändert, setzen Sie die Initialisierung in die Konstruktoren.
  4. Seien Sie konsequent in Ihrer Praxis (die wichtigste Regel).
Kokos
quelle
4
Ich gehe davon aus, dass kokos bedeutet, dass Sie Mitglieder nicht mit ihren Standardwerten (0, false, null usw.) initialisieren sollten, da der Compiler dies für Sie erledigt (1.). Wenn Sie ein Feld jedoch mit einem anderen Wert als dem Standardwert initialisieren möchten, sollten Sie dies in der Deklaration (2.) tun. Ich denke, dass es die Verwendung des Wortes "Standard" sein könnte, die Sie verwirrt.
Ricky Helgesson
95
Ich bin mit Regel 1 nicht einverstanden. Wenn Sie keinen Standardwert angeben (unabhängig davon, ob er vom Compiler initialisiert wurde oder nicht), überlassen Sie es dem Entwickler, zu erraten oder nach der Dokumentation zu suchen, wie der Standardwert für diese bestimmte Sprache lauten würde. Aus Gründen der Lesbarkeit würde ich immer den Standardwert angeben.
James
32
Der Standardwert eines Typs default(T)ist immer der Wert, für den eine interne binäre Darstellung vorliegt 0.
Olivier Jacot-Descombes
15
Unabhängig davon, ob Sie Regel 1 mögen oder nicht, kann sie nicht mit schreibgeschützten Feldern verwendet werden, die zum Zeitpunkt des Abschlusses des Konstruktors explizit initialisiert werden müssen .
Jojo
36
Ich bin nicht einverstanden mit denen, die mit Regel 1 nicht einverstanden sind. Es ist in Ordnung zu erwarten, dass andere die C # -Sprache lernen. So wie wir nicht jede foreachSchleife mit "Dies wiederholt das Folgende für alle Elemente in der Liste" kommentieren , müssen wir die Standardwerte von C # nicht ständig anpassen. Wir müssen auch nicht so tun, als hätte C # eine nicht initialisierte Semantik. Da das Fehlen eines Wertes eine klare Bedeutung hat, ist es in Ordnung, ihn wegzulassen. Wenn es ideal ist, explizit zu sein, sollten Sie immer newneue Delegaten erstellen (wie in C # 1 erforderlich). Aber wer macht das? Die Sprache ist für gewissenhafte Programmierer konzipiert.
Edward Brey
149

In C # spielt es keine Rolle. Die beiden von Ihnen angegebenen Codebeispiele sind absolut gleichwertig. Im ersten Beispiel erstellt der C # -Compiler (oder ist es die CLR?) Einen leeren Konstruktor und initialisiert die Variablen so, als wären sie im Konstruktor (dies hat eine leichte Nuance, die Jon Skeet in den Kommentaren unten erklärt). Wenn es bereits einen Konstruktor gibt, wird jede Initialisierung "oben" in den oberen Bereich verschoben.

In Bezug auf bewährte Verfahren ist das erstere weniger fehleranfällig als das letztere, da jemand leicht einen anderen Konstruktor hinzufügen und vergessen könnte, ihn zu verketten.

Streit
quelle
4
Dies ist nicht korrekt, wenn Sie die Klasse mit GetUninitializedObject initialisieren. Was auch immer im ctor ist, wird nicht berührt, aber die Felddeklarationen werden ausgeführt.
Wolf5
28
Eigentlich ist es wichtig. Wenn der Basisklassenkonstruktor eine virtuelle Methode aufruft (was im Allgemeinen eine schlechte Idee ist, aber passieren kann), die in der abgeleiteten Klasse überschrieben wird, wird die Variable mithilfe von Instanzvariableninitialisierern initialisiert, wenn die Methode aufgerufen wird - während die Initialisierung in der verwendet wird Konstruktor werden sie nicht sein. (Instanzvariableninitialisierer werden ausgeführt, bevor der Basisklassenkonstruktor aufgerufen wird.)
Jon Skeet
2
"Ja wirklich?" Ich schwöre, ich habe diese Informationen aus Richters CLR über C # (2. Ausgabe, glaube ich) abgerufen und das Wesentliche war, dass dies syntaktischer Zucker war (ich habe ihn möglicherweise falsch gelesen?) Und die CLR die Variablen einfach in den Konstruktor eingeklemmt hat. Sie geben jedoch an, dass dies nicht der Fall ist, dh, die Mitgliederinitialisierung kann vor der ctor-Initialisierung ausgelöst werden, wenn ein virtueller In-Base-Ctor aufgerufen wird und die betreffende Klasse überschrieben wird. Habe ich richtig verstanden Hast du das gerade herausgefunden? Verwirrt über diesen aktuellen Kommentar zu einem 5 Jahre alten Beitrag (OMG waren es 5 Jahre?).
Quibblesome
@Quibblesome: Ein untergeordneter Klassenkonstruktor enthält einen verketteten Aufruf an den übergeordneten Konstruktor. Es steht einem Sprachcompiler frei, vorher so viel oder so wenig Code einzuschließen, wie er möchte, vorausgesetzt, der übergeordnete Konstruktor wird auf allen Codepfaden genau einmal aufgerufen, und das vor diesem Aufruf im Aufbau befindliche Objekt wird nur eingeschränkt verwendet. Einer meiner Ärger mit C # ist, dass es zwar Code generieren kann, der Felder initialisiert, und Code, der Konstruktorparameter verwendet, aber keinen Mechanismus zum Initialisieren von Feldern basierend auf Konstruktorparametern bietet.
Supercat
1
@Quibblesome, der frühere ist ein Problem, wenn der Wert zugewiesen und an eine WCF-Methode übergeben wird. Dadurch werden die Daten auf den Standarddatentyp des Modells zurückgesetzt. Die beste Vorgehensweise besteht darin, die Initialisierung im Konstruktor (später) durchzuführen.
Pranesh Janarthanan
16

Die Semantik von C # unterscheidet sich hier geringfügig von Java. In C # wird die Zuweisung in der Deklaration durchgeführt, bevor der Superklassenkonstruktor aufgerufen wird. In Java erfolgt dies unmittelbar danach, wodurch die Verwendung von 'this' ermöglicht wird (besonders nützlich für anonyme innere Klassen), und bedeutet, dass die Semantik der beiden Formen wirklich übereinstimmt.

Wenn Sie können, machen Sie die Felder endgültig.

Tom Hawtin - Tackline
quelle
15

Ich denke, es gibt eine Einschränkung. Ich habe einmal einen solchen Fehler begangen: Innerhalb einer abgeleiteten Klasse habe ich versucht, die von einer abstrakten Basisklasse geerbten Felder bei der Deklaration zu "initialisieren". Das Ergebnis war, dass es zwei Sätze von Feldern gab, eines ist "Basis" und eines ist das neu deklarierte, und das Debuggen hat mich einige Zeit gekostet.

Die Lektion: Um geerbte Felder zu initialisieren , müssen Sie dies im Konstruktor tun.

xji
quelle
Wenn Sie also das Feld durch verweisen derivedObject.InheritedField, bezieht es sich dann auf Ihr Basisfeld oder das abgeleitete?
RayLuo
6

Wenn Sie den Typ in Ihrem Beispiel annehmen, ziehen Sie es definitiv vor, Felder im Konstruktor zu initialisieren. Die Ausnahmefälle sind:

  • Felder in statischen Klassen / Methoden
  • Felder, die als statisch / final / et al

Ich denke immer an die Feldliste oben in einer Klasse als Inhaltsverzeichnis (was hier enthalten ist, nicht wie es verwendet wird) und den Konstruktor als Einführung. Methoden sind natürlich Kapitel.

Noel
quelle
Warum ist es "definitiv" so? Sie haben nur eine Stilvorliebe angegeben, ohne deren Begründung zu erläutern. Oh, warte, egal, laut der Antwort von @quibblesome sind sie "absolut gleichwertig", es ist also wirklich nur eine persönliche Stilpräferenz.
RayLuo
4

Was ist, wenn ich es dir sage, es kommt darauf an?

Ich initialisiere im Allgemeinen alles und mache es auf konsistente Weise. Ja, es ist zu explizit, aber es ist auch etwas einfacher zu warten.

Wenn wir uns Sorgen um die Leistung machen, initialisiere ich nur das, was getan werden muss, und platziere es in den Bereichen, in denen es das Beste für das Geld gibt.

In einem Echtzeitsystem frage ich mich, ob ich überhaupt die Variable oder Konstante brauche.

Und in C ++ mache ich oft so gut wie keine Initialisierung an beiden Stellen und verschiebe sie in eine Init () - Funktion. Warum? Wenn Sie in C ++ etwas initialisieren, das während der Objektkonstruktion eine Ausnahme auslösen kann, öffnen Sie sich für Speicherlecks.

Dan Blair
quelle
4

Es gibt viele und verschiedene Situationen.

Ich brauche nur eine leere Liste

Die Situation ist klar. Ich muss nur meine Liste vorbereiten und verhindern, dass eine Ausnahme ausgelöst wird, wenn jemand der Liste ein Element hinzufügt.

public class CsvFile
{
    private List<CsvRow> lines = new List<CsvRow>();

    public CsvFile()
    {
    }
}

Ich kenne die Werte

Ich weiß genau, welche Werte ich standardmäßig haben möchte oder ich muss eine andere Logik verwenden.

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = new List<string>() {"usernameA", "usernameB"};
    }
}

oder

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = GetDefaultUsers(2);
    }
}

Leere Liste mit möglichen Werten

Manchmal erwarte ich standardmäßig eine leere Liste mit der Möglichkeit, Werte über einen anderen Konstruktor hinzuzufügen.

public class AdminTeam
{
    private List<string> usernames = new List<string>();

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}
Miroslav Holec
quelle
3

In Java bedeutet ein Initialisierer mit der Deklaration, dass das Feld immer auf die gleiche Weise initialisiert wird, unabhängig davon, welcher Konstruktor verwendet wird (wenn Sie mehr als einen haben) oder die Parameter Ihrer Konstruktoren (wenn sie Argumente haben), obwohl ein Konstruktor möglicherweise später Ändern Sie den Wert (falls er nicht endgültig ist). Die Verwendung eines Initialisierers mit einer Deklaration legt dem Leser nahe, dass der initialisierte Wert der Wert ist, den das Feld in allen Fällen hat , unabhängig davon, welcher Konstruktor verwendet wird und unabhängig von den Parametern, die an einen Konstruktor übergeben werden. Verwenden Sie daher einen Initialisierer mit der Deklaration nur, wenn und immer dann, wenn der Wert für alle erstellten Objekte gleich ist.

Raedwald
quelle
3

Das Design von C # legt nahe, dass die Inline-Initialisierung bevorzugt wird oder nicht in der Sprache erfolgt. Jedes Mal, wenn Sie einen Querverweis zwischen verschiedenen Stellen im Code vermeiden können, sind Sie im Allgemeinen besser dran.

Es gibt auch die Frage der Konsistenz mit der statischen Feldinitialisierung, die für eine optimale Leistung inline sein muss. Die Richtlinien für das Framework-Design für das Konstruktordesign lauten wie folgt:

✓ Denken Sie daran, statische Felder inline zu initialisieren, anstatt statische Konstruktoren explizit zu verwenden, da die Laufzeit die Leistung von Typen optimieren kann, die keinen explizit definierten statischen Konstruktor haben.

"Überlegen" bedeutet in diesem Zusammenhang, dies zu tun, es sei denn, es gibt einen guten Grund, dies nicht zu tun. Bei statischen Initialisierungsfeldern ist ein guter Grund, wenn die Initialisierung zu komplex ist, um inline codiert zu werden.

Edward Brey
quelle
2

Konsequent zu sein ist wichtig, aber das ist die Frage, die Sie sich stellen sollten: "Habe ich einen Konstruktor für irgendetwas anderes?"

Normalerweise erstelle ich Modelle für Datenübertragungen, die die Klasse selbst nur als Gehäuse für Variablen verwendet.

In diesen Szenarien habe ich normalerweise keine Methoden oder Konstruktoren. Es wäre für mich albern, einen Konstruktor für den ausschließlichen Zweck der Initialisierung meiner Listen zu erstellen, zumal ich sie gemäß der Deklaration initialisieren kann.

Wie viele andere gesagt haben, hängt es von Ihrer Verwendung ab. Halten Sie es einfach und machen Sie nichts extra, was Sie nicht müssen.

MeanJerry
quelle
2

Betrachten Sie die Situation, in der Sie mehr als einen Konstruktor haben. Wird die Initialisierung für die verschiedenen Konstruktoren unterschiedlich sein? Wenn sie gleich sind, warum dann für jeden Konstruktor wiederholen? Dies steht im Einklang mit der kokos-Anweisung, kann jedoch nicht mit Parametern zusammenhängen. Angenommen, Sie möchten ein Flag behalten, das zeigt, wie das Objekt erstellt wurde. Dann würde dieses Flag für verschiedene Konstruktoren unabhängig von den Konstruktorparametern unterschiedlich initialisiert. Wenn Sie andererseits für jeden Konstruktor dieselbe Initialisierung wiederholen, besteht die Möglichkeit, dass Sie (unbeabsichtigt) den Initialisierungsparameter in einigen Konstruktoren ändern, in anderen jedoch nicht. Das Grundkonzept hier ist also, dass gemeinsamer Code einen gemeinsamen Speicherort haben und möglicherweise nicht an verschiedenen Speicherorten wiederholt werden sollte.

Joe Programmer
quelle
1

Das Festlegen des Werts in der Deklaration hat einen leichten Leistungsvorteil. Wenn Sie es im Konstruktor festlegen, wird es tatsächlich zweimal festgelegt (zuerst auf den Standardwert, dann im ctor zurückgesetzt).

John Meagher
quelle
2
In C # werden Felder immer zuerst auf den Standardwert gesetzt. Das Vorhandensein eines Initialisierers macht keinen Unterschied.
Jeffrey L Whitledge
0

Normalerweise versuche ich, dass der Konstruktor nichts anderes tut, als die Abhängigkeiten abzurufen und die zugehörigen Instanzmitglieder damit zu initialisieren. Dies erleichtert Ihnen das Leben, wenn Sie Ihre Klassen einem Unit-Test unterziehen möchten.

Wenn der Wert, den Sie einer Instanzvariablen zuweisen möchten, von keinem der Parameter beeinflusst wird, die Sie an Ihren Konstruktor übergeben, weisen Sie ihn zur Deklarationszeit zu.

Iker Jimenez
quelle
0

Keine direkte Antwort auf Ihre Frage nach der Best Practice, aber ein wichtiger und verwandter Auffrischungspunkt ist, dass Sie im Fall einer generischen Klassendefinition entweder den Compiler mit Standardwerten initialisieren lassen oder eine spezielle Methode zum Initialisieren von Feldern verwenden müssen auf ihre Standardwerte (wenn dies für die Lesbarkeit des Codes absolut notwendig ist).

class MyGeneric<T>
{
    T data;
    //T data = ""; // <-- ERROR
    //T data = 0; // <-- ERROR
    //T data = null; // <-- ERROR        

    public MyGeneric()
    {
        // All of the above errors would be errors here in constructor as well
    }
}

Die spezielle Methode zum Initialisieren eines generischen Felds auf seinen Standardwert lautet wie folgt:

class MyGeneric<T>
{
    T data = default(T);

    public MyGeneric()
    {           
        // The same method can be used here in constructor
    }
}
user1451111
quelle
0

Wenn Sie keine Logik oder Fehlerbehandlung benötigen:

  • Klassenfelder bei der Deklaration initialisieren

Wenn Sie eine Logik oder Fehlerbehandlung benötigen:

  • Klassenfelder im Konstruktor initialisieren

Dies funktioniert gut, wenn der Initialisierungswert verfügbar ist und die Initialisierung in eine Zeile gestellt werden kann. Diese Form der Initialisierung weist jedoch aufgrund ihrer Einfachheit Einschränkungen auf. Wenn für die Initialisierung eine Logik erforderlich ist (z. B. Fehlerbehandlung oder eine for-Schleife zum Füllen eines komplexen Arrays), ist eine einfache Zuweisung unzureichend. Instanzvariablen können in Konstruktoren initialisiert werden, in denen Fehlerbehandlung oder andere Logik verwendet werden kann.

Von https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html .

Yousha Aleayoub
quelle