Heute habe ich versucht, meinen Kopf um unveränderliche Objekte zu wickeln, die aufeinander verweisen. Ich bin zu dem Schluss gekommen, dass man das unmöglich machen kann, ohne eine faule Auswertung zu verwenden, aber dabei habe ich diesen (meiner Meinung nach) interessanten Code geschrieben.
public class A
{
public string Name { get; private set; }
public B B { get; private set; }
public A()
{
B = new B(this);
Name = "test";
}
}
public class B
{
public A A { get; private set; }
public B(A a)
{
//a.Name is null
A = a;
}
}
Was ich interessant finde, ist, dass ich mir keine andere Möglichkeit vorstellen kann, ein Objekt vom Typ A in einem Zustand zu beobachten, der noch nicht vollständig konstruiert ist und Threads enthält. Warum ist das überhaupt gültig? Gibt es andere Möglichkeiten, den Zustand eines Objekts zu beobachten, das nicht vollständig konstruiert ist?
c#
immutability
Stilgar
quelle
quelle
A
die anB
.ctor übergebene Instanz der Klasse ist nicht vollständig initialisiert. Sie müssen eine neue Instanz vonB
in erstellen,A
nachdem die Instanz vonA
vollständig initialisiert wurde - dies sollte die letzte Zeile in .ctor sein.Antworten:
Warum erwarten Sie, dass es ungültig ist?
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 .
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:
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 einfachs
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:
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.
quelle
this
Verweis auf andere Objekte innerhalb des Konstruktors zu übergeben. Aber warum erlaubt der Compiler dann nicht die Verwendung desthis
Schlüsselworts aus Feldinitialisierern? Beide können teilweise konstruierte Objekte sehen. Diese Frage lieferte keinen Grund für diese Einschränkung. Wenn Sie den Grund kennen, teilen Sie uns dies bitte mit.Ja, dies ist die einzige Möglichkeit für zwei unveränderliche Objekte, sich aufeinander zu beziehen - mindestens eines von ihnen muss das andere nicht vollständig konstruiert sehen.
Es ist im Allgemeinen eine schlechte Idee,
this
sich von Ihrem Konstruktor entziehen zu lassen , aber in Fällen, in denen Sie sicher sind, was beide Konstruktoren tun, und es die einzige Alternative zur Veränderlichkeit ist, denke ich nicht, dass es zu schlecht ist.quelle
this
in dieser Antwort auf eine andere Frage angeboten."Vollständig konstruiert" wird durch Ihren Code definiert, nicht durch die Sprache.
Dies ist eine Variation beim Aufrufen einer virtuellen Methode vom Konstruktor.
Die allgemeine Richtlinie lautet: Tun Sie das nicht .
Um den Begriff "vollständig konstruiert" korrekt zu implementieren, verlassen Sie
this
Ihren Konstruktor nicht.quelle
Wenn Sie die
this
Referenz während des Konstruktors verlieren, können Sie dies tun. Dies kann natürlich zu Problemen führen, wenn Methoden für das unvollständige Objekt aufgerufen werden. Wie für "andere Möglichkeiten, den Zustand eines Objekts zu beobachten, das nicht vollständig konstruiert ist":virtual
Methode in einem Konstruktor aufrufen ; Der Unterklassenkonstruktor wurde noch nicht aufgerufen, daheroverride
kann versucht werden, auf einen unvollständigen Status zuzugreifen (Felder, die in der Unterklasse deklariert oder initialisiert wurden usw.).FormatterServices.GetUninitializedObject
(wodurch ein Objekt erstellt wird, ohne den Konstruktor überhaupt aufzurufen )quelle
Wenn Sie die Initialisierungsreihenfolge berücksichtigen
Durch Upcasting können Sie eindeutig auf die Klasse zugreifen, BEVOR der Konstruktor der abgeleiteten Instanz aufgerufen wird (aus diesem Grund sollten Sie keine virtuellen Methoden von Konstruktoren verwenden. Sie können leicht auf abgeleitete Felder zugreifen, die nicht vom Konstruktor / Konstruktor in der abgeleiteten Klasse initialisiert wurden hätte die abgeleitete Klasse nicht in einen "konsistenten" Zustand bringen können)
quelle
Sie können das Problem vermeiden, indem Sie B zuletzt in Ihrem Konstruktor instanziieren:
public A() { Name = "test"; B = new B(this); }
Wenn das, was Sie vorschlagen, nicht möglich wäre, wäre A nicht unveränderlich.
Edit: behoben, dank Leppie.
quelle
Das Prinzip ist, dass Sie dieses Objekt nicht aus dem Konstruktorkörper entkommen lassen .
Eine andere Möglichkeit, ein solches Problem zu beobachten, besteht darin, virtuelle Methoden im Konstruktor aufzurufen.
quelle
Wie bereits erwähnt, kann der Compiler nicht wissen, an welchem Punkt ein Objekt so gut konstruiert wurde, dass es nützlich ist. Es wird daher davon ausgegangen, dass ein Programmierer, der
this
von einem Konstruktor ausgeht, weiß, ob ein Objekt gut genug konstruiert wurde, um seine Anforderungen zu erfüllen.Ich möchte jedoch hinzufügen, dass für Objekte, die wirklich unveränderlich sein sollen, vermieden werden muss,
this
an einen Code zu übergeben, der den Status eines Feldes untersucht, bevor ihm sein endgültiger Wert zugewiesen wurde. Dies bedeutet, dass esthis
nicht an einen beliebigen externen Code übergeben wird, bedeutet jedoch nicht, dass etwas falsch daran ist, dass sich ein im Bau befindliches Objekt an ein anderes Objekt weitergibt, um eine Rückreferenz zu speichern, die erst nach dem ersten tatsächlich verwendet wird Konstruktor ist abgeschlossen .Wenn man eine Sprache entwirft, um die Konstruktion und Verwendung unveränderlicher Objekte zu erleichtern, kann es hilfreich sein, Methoden als nur während der Konstruktion, nur nach der Konstruktion oder einer der beiden verwendbar zu deklarieren. Felder könnten während der Erstellung als nicht dereferenzierbar und danach schreibgeschützt deklariert werden. Parameter könnten ebenfalls markiert werden, um anzuzeigen, dass sie nicht dereferenzierbar sein sollten. Unter einem solchen System wäre es einem Compiler möglich, die Konstruktion von Datenstrukturen zu ermöglichen, die sich aufeinander beziehen, bei denen sich jedoch keine Eigenschaft ändern könnte, nachdem sie beobachtet wurde. Ich bin mir nicht sicher, ob die Vorteile einer solchen statischen Überprüfung die Kosten überwiegen würden, aber es könnte interessant sein.
Ein verwandtes Merkmal, das hilfreich wäre, wäre übrigens die Möglichkeit, Parameter und Funktionsrückgaben als kurzlebig, rückgabbar oder (standardmäßig) dauerhaft zu deklarieren. Wenn ein Parameter oder eine Funktionsrückgabe als kurzlebig deklariert wurde, konnte er weder in ein Feld kopiert noch als dauerhafter Parameter an eine Methode übergeben werden. Darüber hinaus würde das Übergeben eines kurzlebigen oder rückzahlbaren Werts als rückzahlbarer Parameter an eine Methode dazu führen, dass der Rückgabewert der Funktion die Einschränkungen dieses Werts erbt (wenn eine Funktion zwei rückgabbare Parameter hat, würde ihr Rückgabewert die restriktivere Einschränkung von ihrem erben Parameter). Eine große Schwäche bei Java und .net besteht darin, dass alle Objektreferenzen promiskuitiv sind. Sobald externer Code einen in die Hände bekommt, ist nicht abzusehen, wer damit enden könnte. Wenn Parameter als kurzlebig deklariert werden könnten, Es wäre häufiger möglich, dass Code, der den einzigen Verweis auf etwas enthielt, weiß, dass er den einzigen Verweis enthält, und somit unnötige defensive Kopiervorgänge vermeiden. Darüber hinaus könnten Dinge wie Schließungen recycelt werden, wenn der Compiler wissen könnte, dass nach ihrer Rückkehr keine Verweise auf sie vorhanden sind.
quelle