Es scheint, dass die Thread-Sicherheit immer / oft als der Hauptvorteil der Verwendung unveränderlicher Typen und insbesondere von Sammlungen erwähnt wird.
Ich habe eine Situation, in der ich sicherstellen möchte, dass eine Methode ein Wörterbuch von Zeichenfolgen (die in C # unveränderlich sind) nicht ändert. Ich möchte die Dinge so weit wie möglich einschränken.
Ich bin mir jedoch nicht sicher, ob sich das Hinzufügen einer Abhängigkeit zu einem neuen Paket (Microsoft Immutable Collections) lohnt. Leistung ist auch kein großes Problem.
Ich schätze, meine Frage ist, ob unveränderliche Sammlungen dringend empfohlen werden, wenn es keine harten Leistungsanforderungen und keine Thread-Sicherheitsprobleme gibt. Bedenken Sie, dass die Wertesemantik (wie in meinem Beispiel) auch eine Anforderung sein kann oder nicht.
ConcurrentModificationException
der normalerweise dadurch verursacht wird, dass derselbe Thread die Sammlung im selben Thread mutiert, im Körper einerforeach
Schleife über der selben Sammlung.hashCode()
oder dasequals(Object)
Ergebnis geändert wird, kann dies zu Fehlern bei der Verwendung führenCollections
(in einem Beispiel wurdeHashSet
das Objekt in einem "Bucket" gespeichert und sollte nach der Mutation zu einem anderen wechseln).Antworten:
Unveränderlichkeit vereinfacht die Menge an Informationen, die Sie beim späteren Lesen von Code geistig nachverfolgen müssen . Bei veränderlichen Variablen und insbesondere bei veränderlichen Klassenmitgliedern ist es sehr schwer zu wissen, in welchem Zustand sie sich in der bestimmten Zeile befinden, über die Sie lesen, ohne den Code mit einem Debugger durchlaufen zu müssen. Unveränderliche Daten lassen sich leicht nachvollziehen - sie bleiben immer gleich. Wenn Sie es ändern möchten, müssen Sie einen neuen Wert festlegen.
Ehrlich gesagt würde ich es vorziehen, Dinge standardmäßig unveränderlich zu machen und sie dann in veränderlich zu ändern, wenn nachgewiesen ist, dass dies erforderlich ist, unabhängig davon, ob Sie die Leistung benötigen oder ob ein Algorithmus, den Sie haben, für die Unveränderlichkeit keinen Sinn ergibt.
quelle
val
und nur in sehr, sehr seltenen Fällen stelle ich fest, dass ich etwas in eine ändern mussvar
. Viele der 'Variablen', die ich in einer bestimmten Sprache definiere, sind nur dazu da, einen Wert zu speichern, der das Ergebnis einer Berechnung speichert und nicht aktualisiert werden muss.Ihr Code sollte Ihre Absicht ausdrücken. Wenn Sie nicht möchten, dass ein Objekt nach seiner Erstellung geändert wird, können Sie es nicht mehr ändern.
Unveränderlichkeit hat mehrere Vorteile:
Die Absicht des ursprünglichen Autors wird besser zum Ausdruck gebracht.
Woher wissen Sie, dass im folgenden Code eine Änderung des Namens dazu führt, dass die Anwendung irgendwann später eine Ausnahme generiert?
Es ist einfacher sicherzustellen, dass das Objekt nicht in einem ungültigen Zustand angezeigt wird.
Sie müssen dies in einem Konstruktor steuern und nur dort. Wenn Sie jedoch über eine Reihe von Setzern und Methoden verfügen, die das Objekt ändern, können solche Steuerelemente besonders schwierig werden, insbesondere wenn beispielsweise zwei Felder gleichzeitig geändert werden müssen, damit das Objekt gültig ist.
Beispielsweise ist ein Objekt gültig, wenn die Adresse nicht
null
oder die GPS-Koordinaten nicht angegebennull
sind, es ist jedoch ungültig, wenn sowohl die Adresse als auch die GPS-Koordinaten angegeben sind. Können Sie sich vorstellen, dies zu überprüfen, wenn sowohl die Adresse als auch die GPS-Koordinaten einen Setter haben oder beide veränderlich sind?Parallelität.
Übrigens benötigen Sie in Ihrem Fall keine Pakete von Drittanbietern. .NET Framework enthält bereits eine
ReadOnlyDictionary<TKey, TValue>
Klasse.quelle
Es gibt viele Gründe für die Verwendung der Unveränderlichkeit mit einem Thread. Zum Beispiel
Objekt A enthält Objekt B.
Externer Code fragt Ihr Objekt B ab und Sie geben es zurück.
Nun haben Sie drei mögliche Situationen:
Im dritten Fall erkennt der Benutzercode möglicherweise nicht, was Sie getan haben, und nimmt möglicherweise Änderungen am Objekt vor. Auf diese Weise ändern Sie die internen Daten Ihres Objekts, ohne dass Sie die Kontrolle oder Sichtbarkeit darüber haben.
quelle
Unveränderlichkeit kann auch die Implementierung von Abfallsammlern erheblich vereinfachen. Aus dem Wiki von GHC :
quelle
Wir gehen auf das ein, was KChaloux sehr gut zusammengefasst hat ...
Idealerweise haben Sie zwei Arten von Feldern und daher zwei Arten von Code, der sie verwendet. Beide Felder sind unveränderlich und der Code muss die Veränderlichkeit nicht berücksichtigen. oder Felder können geändert werden, und wir müssen Code schreiben, der entweder einen Snapshot (
int x = p.x
) erstellt oder solche Änderungen ordnungsgemäß verarbeitet.Meiner Erfahrung nach liegt der meiste Code zwischen den beiden, da es sich um optimistischen Code handelt: Er verweist frei auf veränderbare Daten, vorausgesetzt, dass der erste Aufruf
p.x
dasselbe Ergebnis wie der zweite Aufruf hat. Und meistens ist dies der Fall, es sei denn, es stellt sich heraus, dass dies nicht mehr der Fall ist. Hoppla.Also, wirklich, dreh diese Frage um: Was sind meine Gründe, um dies wandelbar zu machen ?
Schreiben Sie defensiven Code? Unveränderlichkeit erspart Ihnen das Kopieren. Schreiben Sie optimistischen Code? Unveränderlichkeit erspart Ihnen den Wahnsinn dieses seltsamen, unmöglichen Fehlers.
quelle
Ein weiterer Vorteil der Unveränderlichkeit besteht darin, dass dies der erste Schritt zum Aufrunden dieser unveränderlichen Objekte zu einem Pool ist. Dann können Sie sie so verwalten, dass Sie nicht mehrere Objekte erstellen, die konzeptionell und semantisch dasselbe darstellen. Ein gutes Beispiel wäre Java's String.
Es ist ein bekanntes Phänomen in der Linguistik, dass einige Wörter häufig vorkommen, möglicherweise auch in einem anderen Kontext. Anstatt mehrere
String
Objekte zu erstellen , können Sie ein unveränderliches Objekt verwenden. Dann müssen Sie jedoch einen Pool-Manager beauftragen, um diese unveränderlichen Objekte zu verwalten.So sparen Sie viel Speicher. Dies ist auch ein interessanter Artikel: http://en.wikipedia.org/wiki/Zipf%27s_law
quelle
In Java, C # und anderen ähnlichen Sprachen können Felder vom Typ "Klasse" entweder zum Identifizieren von Objekten oder zum Einkapseln von Werten oder Zuständen in diesen Objekten verwendet werden. Die Sprachen unterscheiden jedoch nicht zwischen solchen Verwendungen. Angenommen, ein Klassenobjekt
George
hat ein Feld vom Typchar[] chars;
. Dieses Feld kann eine Zeichenfolge enthalten in:Ein Array, das niemals geändert oder einem Code ausgesetzt wird, der es möglicherweise ändert, auf den jedoch möglicherweise externe Verweise vorhanden sind.
Ein Array, zu dem keine externen Referenzen existieren, das George jedoch frei ändern kann.
Ein Array, das George gehört, für das jedoch möglicherweise externe Ansichten existieren, von denen zu erwarten ist, dass sie den aktuellen Status von George darstellen.
Zusätzlich kann die Variable, anstatt eine Zeichenfolge zu kapseln, eine Live-Ansicht in eine Zeichenfolge kapseln, die einem anderen Objekt gehört
Wenn
chars
George derzeit die Zeichenfolge [Wind]chars
einkapselt und diese Zeichenfolge [Zauberstab] einkapseln möchte , gibt es eine Reihe von Dingen, die George tun könnte:A. Konstruieren Sie ein neues Array mit den Zeichen [Zauberstab] und ändern Sie es
chars
, um dieses Array und nicht das alte zu identifizieren.B. Identifizieren Sie auf irgendeine Weise ein bereits vorhandenes Zeichen-Array, das immer die Zeichen [Zauberstab] enthält, und ändern Sie es
chars
, um dieses Array anstelle des alten zu identifizieren.C. Ändern Sie das zweite Zeichen des mit gekennzeichneten Arrays
chars
in eina
.In Fall 1 sind (A) und (B) sichere Wege, um das gewünschte Ergebnis zu erzielen. In dem Fall (2) sind (A) und (C) sicher, aber (B) wäre dies nicht [es würde keine unmittelbaren Probleme verursachen, aber da George davon ausgehen würde, dass er Eigentümer des Arrays ist, würde er davon ausgehen, dass dies der Fall ist könnte das Array nach Belieben ändern]. In Fall (3) würden die Auswahlmöglichkeiten (A) und (B) alle Außenansichten sprengen, und daher ist nur Auswahl (C) richtig. Wenn Sie also wissen möchten, wie Sie die vom Feld eingeschlossene Zeichenfolge ändern können, müssen Sie wissen, um welchen semantischen Feldtyp es sich handelt.
Wenn der
char[]
Code anstelle eines Feldes vom Typ , das eine potenziell veränderbare ZeichenfolgeString
einschließt, den Typ verwendet , der eine unveränderliche Zeichenfolge einschließt, gehen alle oben genannten Probleme verloren. Alle Felder des TypsString
kapseln eine Folge von Zeichen mit einem gemeinsam nutzbaren Objekt, das sich nie ändern wird. Folglich, wenn ein Feld vom TypString
Verkapselt "Wind". Die einzige Möglichkeit, den "Zauberstab" zu verkapseln, besteht darin, ein anderes Objekt zu identifizieren - eines, das den "Zauberstab" enthält. In Fällen, in denen Code den einzigen Verweis auf das Objekt enthält, ist das Mutieren des Objekts möglicherweise effizienter als das Erstellen eines neuen Objekts. Wenn eine Klasse jedoch veränderbar ist, muss zwischen den verschiedenen Möglichkeiten unterschieden werden, mit denen sie Werte einkapseln kann. Persönlich denke ich, dass Apps Hungarian dafür hätte verwendet werden sollen (ich würde die vier Verwendungen vonchar[]
als semantisch unterschiedliche Typen betrachten, obwohl das Typensystem sie als identisch ansieht - genau die Art von Situation, in der Apps Hungarian glänzt), aber seitdem Es war nicht der einfachste Weg, solche Mehrdeutigkeiten zu vermeiden, unveränderliche Typen zu entwerfen, die Werte nur in eine Richtung einschließen.quelle
Es gibt hier einige schöne Beispiele, aber ich wollte mit einigen persönlichen einsteigen, bei denen Unveränderlichkeit eine Menge half. In meinem Fall begann ich mit dem Entwurf einer unveränderlichen gleichzeitigen Datenstruktur, hauptsächlich in der Hoffnung, dass ich in der Lage sein sollte, Code sicher parallel zu überlappenden Lese- und Schreibvorgängen auszuführen, ohne mir Gedanken über die Rennbedingungen machen zu müssen. Es gab einen Vortrag, den John Carmack mich dazu inspirierte, wo er über eine solche Idee sprach. Es ist eine ziemlich grundlegende Struktur und ziemlich trivial zu implementieren:
Natürlich können Sie mit ein paar einfachen Schritten Elemente in konstanter Zeit entfernen und wiedergewinnbare Löcher zurücklassen, und die Blöcke werden derefisiert, wenn sie für eine bestimmte unveränderliche Instanz leer und möglicherweise befreit werden. Um die Struktur zu ändern, müssen Sie jedoch eine "vorübergehende" Version ändern und die an ihr vorgenommenen Änderungen atomar festschreiben, um eine neue unveränderliche Kopie zu erhalten, die die alte nicht berührt. In der neuen Version werden nur neue Kopien der Blöcke erstellt, die diese Änderungen enthalten müssen eindeutig gemacht werden, während die anderen kopiert und gezählt werden.
Aber ich fand es nicht , dassnützlich für Multithreading-Zwecke. Schließlich gibt es immer noch das konzeptionelle Problem, bei dem beispielsweise ein Physiksystem die Physik gleichzeitig anwendet, während ein Spieler versucht, Elemente in einer Welt zu bewegen. Mit welcher unveränderlichen Kopie der transformierten Daten gehen Sie, der vom Spieler transformierten oder der vom physikalischen System transformierten? Daher habe ich keine wirklich gute und einfache Lösung für dieses grundlegende konzeptionelle Problem gefunden, es sei denn, es gibt veränderbare Datenstrukturen, die sich nur auf intelligentere Weise sperren und überlappende Lese- und Schreibvorgänge in denselben Abschnitten des Puffers verhindern, um ein Abwürgen von Threads zu vermeiden. Das scheint John Carmack in seinen Spielen herausgefunden zu haben. Zumindest redet er darüber, als könne er fast eine Lösung sehen, ohne ein Auto von Würmern zu öffnen. Ich bin in dieser Hinsicht nicht so weit gekommen wie er. Alles, was ich sehen kann, sind endlose Designfragen, wenn ich versuche, alles um unveränderliche Objekte herum zu parallelisieren. Ich wünschte, ich könnte einen Tag damit verbringen, an seinem Gehirn herumzusuchen, da die meisten meiner Bemühungen mit den Ideen begannen, die er ausstieß.
Trotzdem fand ich in anderen Bereichen einen enormen Wert dieser unveränderlichen Datenstruktur. Ich verwende es jetzt sogar, um Bilder zu speichern, was wirklich seltsam ist und macht, dass der Direktzugriff einige weitere Anweisungen erfordert (Rechtsverschiebung und bitweise
and
zusammen mit einer Schicht von Zeiger-Indirektion), aber ich werde die folgenden Vorteile behandeln.System rückgängig machen
Einer der unmittelbarsten Orte, an denen ich davon profitierte, war das Undo-System. Das Rückgängigmachen von Systemcode war eines der fehleranfälligsten Dinge in meinem Bereich (Visual FX-Branche), und zwar nicht nur in den Produkten, an denen ich gearbeitet habe, sondern auch in konkurrierenden Produkten (deren Rückgängigmachungssysteme waren auch schuppig), da es so viele verschiedene gab Datentypen, die Sie für das ordnungsgemäße Rückgängigmachen und Wiederherstellen benötigen (Eigenschaftensystem, Änderungen der Netzdaten, Änderungen des Shaders, die nicht auf Eigenschaften basieren, z. B. das Vertauschen von Daten untereinander, Änderungen der Szenenhierarchie, z. usw. usw. usw.).
Die Menge an erforderlichem Rückgängig-Code war also enorm und konkurrierte oft mit der Menge an Code, der das System implementiert, für das das Rückgängig-System Zustandsänderungen aufzeichnen musste. Indem ich mich auf diese Datenstruktur stützte, konnte ich das System zum Rückgängigmachen auf genau dies zurückführen:
Normalerweise wäre der obige Code enorm ineffizient, wenn Ihre Szenendaten Gigabyte umfassen und vollständig kopiert werden. Diese Datenstruktur kopiert jedoch nur oberflächlich Dinge, die nicht geändert wurden, und macht es tatsächlich billig genug, eine unveränderliche Kopie des gesamten Anwendungsstatus zu speichern. So kann ich nun Undo-Systeme so einfach wie den obigen Code implementieren und mich darauf konzentrieren, diese unveränderliche Datenstruktur zu verwenden, um das Kopieren unveränderter Teile des Anwendungsstatus immer billiger und billiger zu machen. Seit ich diese Datenstruktur verwende, haben alle meine persönlichen Projekte Systeme rückgängig gemacht, die nur dieses einfache Muster verwenden.
Jetzt gibt es hier noch etwas Overhead. Das letzte Mal, als ich es gemessen habe, waren es ungefähr 10 Kilobyte, nur um den gesamten Anwendungsstatus flach zu kopieren, ohne Änderungen daran vorzunehmen (dies ist unabhängig von der Komplexität der Szene, da die Szene in einer Hierarchie angeordnet ist. Wenn sich also nichts unter dem Stamm ändert, ändert sich nur der Stamm kopiert wird, ohne in die Kinder hinabsteigen zu müssen). Das ist weit von 0 Byte entfernt, wie es für ein Rückgängig-System erforderlich wäre, das nur Deltas speichert. Bei einem Overhead von 10 Kilobyte Rückgängigmachen pro Vorgang sind dies jedoch nur ein Megabyte pro 100 Benutzervorgänge. Außerdem könnte ich das möglicherweise in Zukunft noch weiter reduzieren, wenn nötig.
Ausnahmesicherheit
Ausnahmesicherheit bei einer komplexen Anwendung ist keine Kleinigkeit. Wenn Ihr Anwendungsstatus jedoch unveränderlich ist und Sie nur transiente Objekte verwenden, um atomare Änderungstransaktionen festzuschreiben, ist dies von Natur aus ausnahmesicher, da der Transient verworfen wird, wenn ein Teil des Codes ausgelöst wird, bevor eine neue unveränderliche Kopie erstellt wird . Damit wird eines der schwierigsten Dinge, die ich in einer komplexen C ++ - Codebasis immer richtig gefunden habe, banalisiert.
Zu viele Leute verwenden in C ++ nur RAII-konforme Ressourcen und denken, dass dies ausnahmesicher ist. Dies ist häufig nicht der Fall, da eine Funktion in der Regel Nebenwirkungen hervorrufen kann, die über den lokalen Bereich hinausgehen. In diesen Fällen müssen Sie sich im Allgemeinen mit Scope Guards und ausgefeilter Rollback-Logik befassen. Diese Datenstruktur hat es so gemacht, dass ich mich oft nicht darum kümmern muss, da die Funktionen keine Nebenwirkungen verursachen. Sie geben transformierte unveränderliche Kopien des Anwendungsstatus zurück, anstatt den Anwendungsstatus zu transformieren.
Zerstörungsfreie Bearbeitung
Die zerstörungsfreie Bearbeitung besteht im Wesentlichen aus dem Zusammenfügen / Stapeln / Verbinden von Vorgängen, ohne die Daten des ursprünglichen Benutzers zu berühren (nur Eingabe- und Ausgabedaten, ohne Eingabe zu berühren). Die Implementierung mit einer einfachen Bildanwendung wie Photoshop ist in der Regel trivial und profitiert möglicherweise nicht so stark von dieser Datenstruktur, da bei vielen Vorgängen möglicherweise nur alle Pixel des gesamten Bilds transformiert werden sollen.
Bei der zerstörungsfreien Netzbearbeitung beispielsweise möchten viele Vorgänge jedoch häufig nur einen Teil des Netzes transformieren. Eine Operation möchte hier möglicherweise nur einige Scheitelpunkte verschieben. Ein anderer möchte dort möglicherweise nur einige Polygone unterteilen. Hier hilft die unveränderliche Datenstruktur einer Tonne dabei, die Notwendigkeit zu vermeiden, eine vollständige Kopie des gesamten Netzes anzufertigen, nur um eine neue Version des Netzes mit einem kleinen Teil davon, der geändert wurde, zurückzugeben.
Minimierung von Nebenwirkungen
Mit diesen Strukturen ist es auch einfach, Funktionen zu schreiben, mit denen Nebenwirkungen minimiert werden, ohne dass eine enorme Leistungseinbuße entsteht. Ich habe immer mehr Funktionen geschrieben, die heutzutage nur ganze unveränderliche Datenstrukturen wertmäßig zurückgeben, ohne dass Nebenwirkungen auftreten, auch wenn dies etwas verschwenderisch erscheint.
Beispielsweise könnte die Versuchung, eine Reihe von Positionen zu transformieren, typischerweise darin bestehen, eine Matrix und eine Liste von Objekten zu akzeptieren und diese auf veränderbare Weise zu transformieren. In diesen Tagen stelle ich fest, dass ich gerade eine neue Liste von Objekten zurückgebe.
Wenn Ihr System über mehr Funktionen wie diese verfügt, die keine Nebenwirkungen verursachen, ist es auf jeden Fall einfacher, über die Richtigkeit des Systems nachzudenken und seine Richtigkeit zu testen.
Die Vorteile billiger Kopien
Dies sind die Bereiche, in denen ich unveränderliche Datenstrukturen (oder persistente Datenstrukturen) am meisten genutzt habe. Anfangs war ich auch etwas übereifrig und erstellte einen unveränderlichen Baum, eine unveränderliche verknüpfte Liste und eine unveränderliche Hash-Tabelle, aber im Laufe der Zeit fand ich selten so viel Verwendung für diese. In der obigen Abbildung habe ich hauptsächlich den klobigen, unveränderlichen Array-ähnlichen Container verwendet.
Ich habe auch immer noch eine Menge Code, der mit veränderlichen Dateien arbeitet (finde es eine praktische Notwendigkeit, zumindest für Code auf niedriger Ebene), aber der Hauptanwendungsstatus ist eine unveränderliche Hierarchie, die von einer unveränderlichen Szene zu unveränderlichen Komponenten in ihr hinunterschlägt. Einige der billigeren Komponenten werden immer noch vollständig kopiert, aber die teuersten, wie Maschen und Bilder, verwenden die unveränderliche Struktur, um nur die teilweise billigen Kopien der Teile zu ermöglichen, die transformiert werden mussten.
quelle
Es gibt bereits viele gute Antworten. Dies ist nur eine zusätzliche Information, die sich auf .NET bezieht. Ich habe in alten .NET-Blog-Posts gebuddelt und eine schöne Zusammenfassung der Vorteile aus der Sicht der Entwickler von Microsoft Immutable Collections gefunden:
quelle