Ich habe eine seltsame Angewohnheit, wie es scheint ... zumindest laut meinem Kollegen. Wir haben zusammen an einem kleinen Projekt gearbeitet. Ich habe die Klassen so geschrieben (vereinfachtes Beispiel):
[Serializable()]
public class Foo
{
public Foo()
{ }
private Bar _bar;
public Bar Bar
{
get
{
if (_bar == null)
_bar = new Bar();
return _bar;
}
set { _bar = value; }
}
}
Im Grunde genommen initialisiere ich ein Feld nur, wenn ein Getter aufgerufen wird und das Feld immer noch null ist. Ich dachte, dies würde die Überlastung verringern, indem keine Eigenschaften initialisiert werden, die nirgendwo verwendet werden.
ETA: Der Grund, warum ich dies getan habe, ist, dass meine Klasse mehrere Eigenschaften hat, die eine Instanz einer anderen Klasse zurückgeben, die wiederum auch Eigenschaften mit noch mehr Klassen haben, und so weiter. Wenn Sie den Konstruktor für die oberste Klasse aufrufen, werden anschließend alle Konstruktoren für alle diese Klassen aufgerufen, wenn nicht immer alle benötigt werden.
Gibt es andere Einwände gegen diese Praxis als persönliche Vorlieben?
UPDATE: Ich habe die vielen unterschiedlichen Meinungen in Bezug auf diese Frage berücksichtigt und werde zu meiner akzeptierten Antwort stehen. Jetzt habe ich das Konzept jedoch viel besser verstanden und kann entscheiden, wann ich es verwenden soll und wann nicht.
Nachteile:
- Thread-Sicherheitsprobleme
- Eine "Setter" -Anforderung nicht befolgen, wenn der übergebene Wert null ist
- Mikrooptimierungen
- Die Ausnahmebehandlung sollte in einem Konstruktor erfolgen
- Im Klassencode muss nach Null gesucht werden
Vorteile:
- Mikrooptimierungen
- Eigenschaften geben niemals null zurück
- Verzögern oder vermeiden Sie das Laden "schwerer" Objekte
Die meisten Nachteile gelten nicht für meine aktuelle Bibliothek, ich müsste jedoch testen, ob die "Mikrooptimierungen" überhaupt etwas optimieren.
LETZTES UPDATE:
Okay, ich habe meine Antwort geändert. Meine ursprüngliche Frage war, ob dies eine gute Angewohnheit ist oder nicht. Und ich bin jetzt davon überzeugt, dass es nicht so ist. Vielleicht werde ich es in einigen Teilen meines aktuellen Codes noch verwenden, aber nicht unbedingt und definitiv nicht die ganze Zeit. Also werde ich meine Gewohnheit verlieren und darüber nachdenken, bevor ich es benutze. Vielen Dank an alle!
quelle
Antworten:
Was Sie hier haben, ist eine - naive - Implementierung der "faulen Initialisierung".
Kurze Antwort:
Die bedingungslose Verwendung der verzögerten Initialisierung ist keine gute Idee. Es hat seine Plätze, aber man muss die Auswirkungen dieser Lösung berücksichtigen.
Hintergrund und Erklärung:
Konkrete Implementierung:
Schauen wir uns zunächst Ihr konkretes Beispiel an und warum ich die Implementierung für naiv halte:
Es verstößt gegen das Prinzip der geringsten Überraschung (POLS) . Wenn einer Eigenschaft ein Wert zugewiesen wird, wird erwartet, dass dieser Wert zurückgegeben wird. In Ihrer Implementierung ist dies nicht der Fall für
null
:foo.Bar
verschiedenen Threads können möglicherweise zwei verschiedene Instanzen von erhalten,Bar
und einer von ihnen hat keine Verbindung zurFoo
Instanz. Alle an dieserBar
Instanz vorgenommenen Änderungen gehen stillschweigend verloren.Dies ist ein weiterer Fall eines Verstoßes gegen POLS. Wenn nur auf den gespeicherten Wert einer Eigenschaft zugegriffen wird, wird erwartet, dass sie threadsicher ist. Während Sie argumentieren könnten, dass die Klasse einfach nicht threadsicher ist - einschließlich des Getters Ihres Eigentums -, müssten Sie dies ordnungsgemäß dokumentieren, da dies nicht der Normalfall ist. Darüber hinaus ist die Einführung dieses Themas nicht erforderlich, wie wir in Kürze sehen werden.
Im Allgemeinen:
Es ist jetzt an der Zeit, die verzögerte Initialisierung im Allgemeinen zu betrachten: Die
verzögerte Initialisierung wird normalerweise verwendet, um die Erstellung von Objekten zu verzögern, deren Erstellung lange dauert oder die nach ihrer vollständigen Erstellung viel Speicher benötigen.
Dies ist ein sehr wichtiger Grund für die Verwendung der verzögerten Initialisierung.
Solche Eigenschaften haben jedoch normalerweise keine Setter, wodurch das oben erwähnte erste Problem beseitigt wird.
Darüber hinaus würde eine thread-sichere Implementierung verwendet
Lazy<T>
, um das zweite Problem zu vermeiden.Selbst wenn diese beiden Punkte bei der Implementierung einer Lazy-Eigenschaft berücksichtigt werden, sind die folgenden Punkte allgemeine Probleme dieses Musters:
Die Konstruktion des Objekts kann nicht erfolgreich sein, was zu einer Ausnahme von einem Property Getter führt. Dies ist eine weitere Verletzung von POLS und sollte daher vermieden werden. Selbst der Abschnitt über Eigenschaften in den "Entwurfsrichtlinien für die Entwicklung von Klassenbibliotheken" besagt ausdrücklich, dass Eigenschafts-Getter keine Ausnahmen auslösen sollten:
Automatische Optimierungen durch den Compiler sind beeinträchtigt, nämlich Inlining und Verzweigungsvorhersage. Eine ausführliche Erklärung finden Sie in der Antwort von Bill K.
Die Schlussfolgerung dieser Punkte lautet wie folgt:
Für jede einzelne Eigenschaft, die träge implementiert wird, sollten Sie diese Punkte berücksichtigt haben.
Dies bedeutet, dass es sich um eine Einzelfallentscheidung handelt, die nicht als allgemeine Best Practice angesehen werden kann.
Dieses Muster hat seinen Platz, ist jedoch keine allgemeine Best Practice bei der Implementierung von Klassen. Es sollte aus den oben genannten Gründen nicht unbedingt verwendet werden .
In diesem Abschnitt möchte ich einige der Punkte diskutieren, die andere als Argumente für die bedingungslose Verwendung der verzögerten Initialisierung vorgebracht haben:
Serialisierung:
EricJ gibt in einem Kommentar an:
Es gibt mehrere Probleme mit diesem Argument:
Mikrooptimierung: Ihr Hauptargument ist, dass Sie die Objekte nur dann erstellen möchten, wenn tatsächlich jemand darauf zugreift. Sie sprechen also tatsächlich über die Optimierung der Speichernutzung.
Ich stimme diesem Argument aus folgenden Gründen nicht zu:
Ich erkenne die Tatsache an, dass diese Art der Optimierung manchmal gerechtfertigt ist. Aber selbst in diesen Fällen scheint eine verzögerte Initialisierung nicht die richtige Lösung zu sein. Es gibt zwei Gründe, die dagegen sprechen:
quelle
null
Fall wurde viel stärker. :-)Es ist eine gute Wahl für das Design. Sehr empfehlenswert für Bibliothekscode oder Kernklassen.
Es wird durch eine "verzögerte Initialisierung" oder "verzögerte Initialisierung" bezeichnet und wird im Allgemeinen von allen als eine gute Wahl für das Design angesehen.
Wenn Sie in der Deklaration von Variablen oder Konstruktoren auf Klassenebene initialisieren, haben Sie beim Erstellen Ihres Objekts zunächst den Aufwand, eine Ressource zu erstellen, die möglicherweise nie verwendet wird.
Zweitens wird die Ressource nur bei Bedarf erstellt.
Drittens vermeiden Sie Müll, der ein Objekt sammelt, das nicht verwendet wurde.
Schließlich ist es einfacher, Initialisierungsausnahmen zu behandeln, die in der Eigenschaft auftreten können, als Ausnahmen, die während der Initialisierung von Variablen auf Klassenebene oder des Konstruktors auftreten.
Es gibt Ausnahmen von dieser Regel.
Das Leistungsargument der zusätzlichen Prüfung für die Initialisierung in der Eigenschaft "get" ist unbedeutend. Das Initialisieren und Entsorgen eines Objekts ist ein bedeutenderer Leistungseinbruch als eine einfache Nullzeigerprüfung mit einem Sprung.
Entwurfsrichtlinien für die Entwicklung von Klassenbibliotheken unter http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
Hinsichtlich
Lazy<T>
Die generische
Lazy<T>
Klasse wurde genau für die Anforderungen des Posters erstellt (siehe Lazy Initialization unter http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx) . Wenn Sie ältere Versionen von .NET haben, müssen Sie das in der Frage dargestellte Codemuster verwenden. Dieses Codemuster ist so verbreitet geworden, dass Microsoft es für angebracht hielt, eine Klasse in die neuesten .NET-Bibliotheken aufzunehmen, um die Implementierung des Musters zu vereinfachen. Wenn Ihre Implementierung Thread-Sicherheit benötigt, müssen Sie diese hinzufügen.Primitive Datentypen und einfache Klassen
Offensichtlich werden Sie die Lazy-Initialisierung nicht für primitive Datentypen oder einfache Klassen verwenden
List<string>
.Bevor ich über Lazy kommentiere
Lazy<T>
wurde in .NET 4.0 eingeführt. Fügen Sie daher bitte keinen weiteren Kommentar zu dieser Klasse hinzu.Vor dem Kommentieren von Mikrooptimierungen
Wenn Sie Bibliotheken erstellen, müssen Sie alle Optimierungen berücksichtigen. In den .NET-Klassen sehen Sie beispielsweise Bit-Arrays, die für boolesche Klassenvariablen im gesamten Code verwendet werden, um den Speicherverbrauch und die Speicherfragmentierung zu reduzieren, um nur zwei "Mikrooptimierungen" zu nennen.
In Bezug auf Benutzeroberflächen
Sie werden die verzögerte Initialisierung nicht für Klassen verwenden, die direkt von der Benutzeroberfläche verwendet werden. Letzte Woche habe ich den größten Teil eines Tages damit verbracht, das langsame Laden von acht Sammlungen zu entfernen, die in einem Ansichtsmodell für Kombinationsfelder verwendet wurden. Ich habe eine
LookupManager
, die das verzögerte Laden und Zwischenspeichern von Sammlungen übernimmt, die von jedem Benutzeroberflächenelement benötigt werden."Setter"
Ich habe noch nie eine Set-Eigenschaft ("Setter") für eine faul geladene Eigenschaft verwendet. Daher würden Sie niemals zulassen
foo.Bar = null;
. Wenn Sie festlegen müssen,Bar
würde ich eine Methode namens aufgerufenSetBar(Bar value)
und keine Lazy-Initialisierung verwendenSammlungen
Klassenerfassungseigenschaften werden bei der Deklaration immer initialisiert, da sie niemals null sein sollten.
Komplexe Klassen
Lassen Sie mich das anders wiederholen, Sie verwenden die Lazy-Initialisierung für komplexe Klassen. Welches sind in der Regel schlecht gestaltete Klassen.
zuletzt
Ich habe nie gesagt, dies für alle Klassen oder in allen Fällen zu tun. Es ist eine schlechte Angewohnheit.
quelle
Denken Sie darüber nach, ein solches Muster mit zu implementieren
Lazy<T>
?Zusätzlich zur einfachen Erstellung von verzögert geladenen Objekten erhalten Sie Thread-Sicherheit, während das Objekt initialisiert wird:
Wie andere sagten, laden Sie Objekte träge, wenn sie wirklich ressourcenintensiv sind oder es einige Zeit dauert, sie während der Objektkonstruktionszeit zu laden.
quelle
Lazy<T>
und es unterlassen, die Art und Weise zu verwenden, wie ich es immer getan habe.Making the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
Ich denke, es hängt davon ab, was Sie initialisieren. Ich würde es wahrscheinlich nicht für eine Liste tun, da die Baukosten ziemlich gering sind, so dass es in den Konstruktor gehen kann. Aber wenn es eine vorab ausgefüllte Liste wäre, würde ich es wahrscheinlich nicht tun, bis sie zum ersten Mal benötigt würde.
Wenn die Baukosten die Kosten für eine bedingte Überprüfung jedes Zugriffs überwiegen, erstellen Sie sie grundsätzlich faul. Wenn nicht, machen Sie es im Konstruktor.
quelle
Der Nachteil, den ich sehen kann, ist, dass wenn Sie fragen möchten, ob Bars null ist, dies niemals der Fall sein würde und Sie die Liste dort erstellen würden.
quelle
null
ein, bekommen es aber nicht zurück. Normalerweise nehmen Sie an, dass Sie denselben Wert zurückerhalten, den Sie in eine Eigenschaft eingegeben haben.Eine verzögerte Instanziierung / Initialisierung ist ein perfektes Muster. Beachten Sie jedoch, dass die Verbraucher Ihrer API in der Regel nicht erwarten, dass Getter und Setter vom POV des Endbenutzers erkennbare Zeit in Anspruch nehmen (oder fehlschlagen).
quelle
Ich wollte gerade Daniels Antwort kommentieren, aber ich glaube ehrlich gesagt nicht, dass es weit genug geht.
Obwohl dies ein sehr gutes Muster ist, das in bestimmten Situationen verwendet werden kann (z. B. wenn das Objekt aus der Datenbank initialisiert wird), ist es eine SCHRECKLICHE Angewohnheit, sich darauf einzulassen.
Eines der besten Dinge an einem Objekt ist, dass es eine sichere, vertrauenswürdige Umgebung bietet. Der beste Fall ist, wenn Sie so viele Felder wie möglich "Final" machen und sie alle mit dem Konstruktor ausfüllen. Dies macht Ihre Klasse ziemlich kugelsicher. Das Ändern von Feldern durch Setter ist etwas weniger, aber nicht schrecklich. Zum Beispiel:
Mit Ihrem Muster würde die toString-Methode folgendermaßen aussehen:
Darüber hinaus benötigen Sie überall dort, wo Sie möglicherweise dieses Objekt in Ihrer Klasse verwenden, Nullprüfungen. (Außerhalb Ihrer Klasse ist dies aufgrund der Nullprüfung im Getter sicher, aber Sie sollten hauptsächlich Ihre Klassenmitglieder innerhalb der Klasse verwenden.)
Auch Ihre Klasse befindet sich ständig in einem unsicheren Zustand. Wenn Sie beispielsweise beschlossen haben, diese Klasse durch Hinzufügen einiger Anmerkungen zu einer Klasse im Ruhezustand zu machen, wie würden Sie dies tun?
Wenn Sie eine Entscheidung treffen, die auf einer Mikrooptomisierung ohne Anforderungen und Tests basiert, ist dies mit ziemlicher Sicherheit die falsche Entscheidung. Tatsächlich besteht eine sehr gute Chance, dass Ihr Muster das System selbst unter den idealsten Umständen tatsächlich verlangsamt, da die if-Anweisung einen Verzweigungsvorhersagefehler auf der CPU verursachen kann, der die Dinge viele, viele Male langsamer verlangsamt als Zuweisen eines Werts im Konstruktor, es sei denn, das von Ihnen erstellte Objekt ist ziemlich komplex oder stammt aus einer entfernten Datenquelle.
Ein Beispiel für das Brance-Vorhersageproblem (das wiederholt und nicht nur einmal auftritt) finden Sie in der ersten Antwort auf diese großartige Frage: Warum ist es schneller, ein sortiertes Array zu verarbeiten als ein unsortiertes Array?
quelle
toString()
aufgerufengetName()
, nichtname
direkt verwendet.Lassen Sie mich noch einen Punkt zu vielen guten Punkten hinzufügen, die von anderen gemacht wurden ...
Der Debugger wertet ( standardmäßig ) die Eigenschaften aus, wenn er den Code durchläuft. Dies kann möglicherweise
Bar
schneller instanziieren, als dies normalerweise durch einfaches Ausführen des Codes der Fall wäre. Mit anderen Worten, das bloße Debuggen verändert die Ausführung des Programms.Dies kann ein Problem sein oder auch nicht (abhängig von den Nebenwirkungen), ist jedoch zu beachten.
quelle
Sind Sie sicher, dass Foo überhaupt etwas instanziieren sollte?
Für mich scheint es stinkend (wenn auch nicht unbedingt falsch ), Foo überhaupt etwas instanziieren zu lassen. Sofern es nicht Foos ausdrücklicher Zweck ist, eine Fabrik zu sein, sollte es nicht seine eigenen Mitarbeiter instanziieren, sondern sie in seinen Konstruktor injizieren lassen .
Wenn jedoch Foos Zweck darin besteht, Instanzen vom Typ Bar zu erstellen, sehe ich nichts Falsches daran, es faul zu machen.
quelle