Warum ist das überhaupt gültig?
Warum erwarten Sie, dass es ungültig ist?
Weil ein Konstruktor sicherstellen soll, dass der darin enthaltene Code ausgeführt wird, bevor externer Code den Status des Objekts beobachten kann.
Richtig. Aber der Compiler ist nicht verantwortlich für diese Invarianten beibehalten wird . Du bist . Wenn Sie Code schreiben, der diese Invariante bricht, und es tut weh, wenn Sie das tun, dann hören Sie damit auf .
Gibt es andere Möglichkeiten, den Zustand eines Objekts zu beobachten, das nicht vollständig konstruiert ist?
Sicher. Bei Referenztypen müssen alle "dies" offensichtlich aus dem Konstruktor übergeben werden, da der einzige Benutzercode, der die Referenz auf den Speicher enthält, der Konstruktor ist. Einige Möglichkeiten, wie der Konstruktor "dies" auslaufen kann, sind:
- Fügen Sie "this" in ein statisches Feld ein und verweisen Sie von einem anderen Thread darauf
- Führen Sie einen Methoden- oder Konstruktoraufruf durch und übergeben Sie "this" als Argument
- einen virtuellen Aufruf ausführen - besonders unangenehm, wenn die virtuelle Methode von einer abgeleiteten Klasse überschrieben wird, da sie dann ausgeführt wird, bevor der ctor-Body der abgeleiteten Klasse ausgeführt wird.
Ich sagte, dass der einzige Benutzercode , der eine Referenz enthält, der ctor ist, aber natürlich enthält der Garbage Collector auch eine Referenz. Eine andere interessante Art und Weise, wie beobachtet werden kann, dass sich ein Objekt in einem halbkonstruierten Zustand befindet, besteht darin, dass das Objekt einen Destruktor hat und der Konstruktor eine Ausnahme auslöst (oder eine asynchrone Ausnahme wie einen Thread-Abbruch erhält; dazu später mehr). ) In diesem Fall ist das Objekt kurz vor dem Tod und muss daher finalisiert werden, aber der Finalizer-Thread kann den halb initialisierten Zustand des Objekts sehen. Und jetzt sind wir wieder im Benutzercode, der das halb konstruierte Objekt sehen kann!
Destruktoren müssen angesichts dieses Szenarios robust sein. Ein Destruktor darf nicht von einer Invariante des vom Konstruktor eingerichteten Objekts abhängen, da das zerstörte Objekt möglicherweise nie vollständig konstruiert wurde.
Eine andere verrückte Möglichkeit, ein halb konstruiertes Objekt durch externen Code zu beobachten, besteht natürlich darin, dass der Destruktor das halb initialisierte Objekt im obigen Szenario sieht und dann einen Verweis auf dieses Objekt in ein statisches Feld kopiert , wodurch sichergestellt wird, dass die Hälfte -konstruiertes, halbfertiges Objekt wird vor dem Tod gerettet. Bitte tue das nicht. Wie ich schon sagte, wenn es weh tut, tu es nicht.
Wenn Sie sich im Konstruktor eines Wertetyps befinden, sind die Dinge im Grunde die gleichen, aber es gibt einige kleine Unterschiede im Mechanismus. Die Sprache erfordert, dass ein Konstruktoraufruf für einen Werttyp eine temporäre Variable erstellt, auf die nur der Ctor Zugriff hat, diese Variable mutiert und dann eine Strukturkopie des mutierten Werts in den tatsächlichen Speicher erstellt. Dies stellt sicher, dass sich der endgültige Speicher nicht in einem halb mutierten Zustand befindet, wenn der Konstruktor wirft.
Beachten Sie, dass seit struct Kopien sind nicht atomar sein garantiert, es ist möglich , ein anderer Thread die Lagerung in einem halbmutiertes Zustand zu sehen; Verwenden Sie Schlösser richtig, wenn Sie sich in dieser Situation befinden. Es ist auch möglich, dass eine asynchrone Ausnahme wie ein Thread-Abbruch zur Hälfte einer Strukturkopie ausgelöst wird. Diese Nichtatomaritätsprobleme treten unabhängig davon auf, ob die Kopie von einer temporären oder einer "regulären" Kopie stammt. Und im Allgemeinen werden nur sehr wenige Invarianten beibehalten, wenn es asynchrone Ausnahmen gibt.
In der Praxis optimiert der C # -Compiler die temporäre Zuordnung und kopiert sie, wenn er feststellen kann, dass dieses Szenario nicht möglich ist. Wenn der neue Wert beispielsweise ein lokales Element initialisiert, das nicht von einem Lambda und nicht in einem Iteratorblock geschlossen wird, S s = new S(123);
mutiert er einfach s
direkt.
Weitere Informationen zur Funktionsweise von Werttypkonstruktoren finden Sie unter:
Einen weiteren Mythos über Werttypen entlarven
Weitere Informationen darüber, wie die C # -Sprachensemantik versucht, Sie vor sich selbst zu retten, finden Sie unter:
Warum werden Initialisierer als Konstruktoren in der entgegengesetzten Reihenfolge ausgeführt? Teil eins
Warum werden Initialisierer als Konstruktoren in der entgegengesetzten Reihenfolge ausgeführt? Zweiter Teil
Ich scheine von dem vorliegenden Thema abgewichen zu sein. In einer Struktur können Sie natürlich beobachten, dass ein Objekt auf die gleiche Weise zur Hälfte konstruiert wird - kopieren Sie das halb konstruierte Objekt in ein statisches Feld, rufen Sie eine Methode mit "this" als Argument auf und so weiter. (Offensichtlich ist das Aufrufen einer virtuellen Methode für einen abgeleiteten Typ bei Strukturen kein Problem.) Und wie gesagt, die Kopie vom temporären zum endgültigen Speicher ist nicht atomar, und daher kann ein anderer Thread die halb kopierte Struktur beobachten.
Betrachten wir nun die Hauptursache Ihrer Frage: Wie stellen Sie unveränderliche Objekte her, die aufeinander verweisen?
Wie Sie festgestellt haben, ist dies normalerweise nicht der Fall. Wenn Sie zwei unveränderliche Objekte haben, die aufeinander verweisen, bilden sie logischerweise einen gerichteten zyklischen Graphen . Sie könnten einfach einen unveränderlichen gerichteten Graphen erstellen! Das ist ganz einfach. Ein unveränderlicher gerichteter Graph besteht aus:
- Eine unveränderliche Liste unveränderlicher Knoten, von denen jeder einen Wert enthält.
- Eine unveränderliche Liste unveränderlicher Knotenpaare, von denen jedes den Start- und Endpunkt einer Diagrammkante hat.
Die Art und Weise, wie Sie die Knoten A und B "referenzieren", ist nun:
A = new Node("A");
B = new Node("B");
G = Graph.Empty.AddNode(A).AddNode(B).AddEdge(A, B).AddEdge(B, A);
Und wenn Sie fertig sind, haben Sie eine Grafik, in der A und B sich gegenseitig "referenzieren".
Das Problem ist natürlich, dass Sie nicht von A nach B gelangen können, ohne G in der Hand zu haben. Diese zusätzliche Indirektionsebene ist möglicherweise nicht akzeptabel.