Lohnt sich Unveränderlichkeit, wenn keine Parallelität besteht?


53

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.


1
Gleichzeitige Änderungen müssen keine Threads bedeuten. Schauen Sie sich nur den passenden Namen an, ConcurrentModificationExceptionder normalerweise dadurch verursacht wird, dass derselbe Thread die Sammlung im selben Thread mutiert, im Körper einer foreachSchleife über der selben Sammlung.

1
Ich meine, du liegst nicht falsch, aber das ist etwas anderes als das, was OP verlangt. Diese Ausnahme wird ausgelöst, da Änderungen während der Aufzählung nicht zulässig sind. Wenn Sie beispielsweise ein ConcurrentDictionary verwenden, tritt dieser Fehler weiterhin auf.
Edthethird

13
Vielleicht sollten Sie sich auch die umgekehrte Frage stellen: Wann lohnt sich Veränderbarkeit?
Giorgio

2
Wenn sich die Mutabilität auf Java auswirkt hashCode()oder das equals(Object)Ergebnis geändert wird, kann dies zu Fehlern bei der Verwendung führen Collections(in einem Beispiel wurde HashSetdas Objekt in einem "Bucket" gespeichert und sollte nach der Mutation zu einem anderen wechseln).
SJuan76

2
@ DavorŽdralo Was die Abstraktionen von Hochsprachen angeht, ist die allgegenwärtige Unveränderlichkeit eher gering. Es ist nur eine natürliche Erweiterung der sehr verbreiteten Abstraktion (sogar in C vorhanden), "temporäre Werte" zu erzeugen und stillschweigend zu verwerfen. Vielleicht wollen Sie damit sagen, dass es eine ineffiziente Art ist, eine CPU zu nutzen, aber dieses Argument weist auch Mängel auf: Mutabilitätsfreundliche, aber dynamische Sprachen sind oft schlechter als unveränderliche, aber statische Sprachen, zum Teil, weil es einige clevere (aber letztendlich recht einfache) Sprachen gibt. Tricks, um Programme zu optimieren, die unveränderliche Daten jonglieren: Lineare Typen, Entwaldung usw.

Antworten:


101

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.


23
+1 Gleichzeitigkeit ist gleichzeitige Mutation, aber Mutation, die sich über die Zeit ausbreitet, kann genauso schwer zu
beurteilen sein

20
Um dies zu erweitern: Eine Funktion, die sich auf eine veränderbare Variable stützt, kann als zusätzliches verstecktes Argument betrachtet werden, das der aktuelle Wert der veränderbaren Variablen ist. Jede Funktion, die eine veränderbare Variable ändert, kann ebenfalls als Erzeugung eines zusätzlichen Rückgabewerts angesehen werden, dh des neuen Werts des veränderbaren Zustands. Wenn Sie sich einen Code ansehen, wissen Sie nicht, ob er vom veränderlichen Zustand abhängt oder diesen ändert. Sie müssen ihn also herausfinden und die Änderungen mental nachverfolgen. Dies führt auch eine Kopplung zwischen zwei beliebigen Codeteilen ein, die einen veränderlichen Zustand aufweisen, und die Kopplung ist schlecht.
Doval

29
@Mehrdad People hat es auch über Jahrzehnte geschafft, große Programme in der Montage auf den Weg zu bringen. Dann machten wir ein paar Jahrzehnte C.
Doval

11
@Mehrdad Das Kopieren ganzer Objekte ist keine gute Option, wenn die Objekte groß sind. Ich verstehe nicht, warum die Größenordnung, um die es bei der Verbesserung geht, von Bedeutung ist. Würden Sie eine Produktivitätsverbesserung von 20% (Anmerkung: willkürliche Zahl) ablehnen, nur weil es sich nicht um eine dreistellige Verbesserung handelt? Unveränderlichkeit ist eine vernünftige Voreinstellung . Sie können davon abweichen, aber Sie brauchen einen Grund.
Doval

9
@Giorgio Scala hat mir klar gemacht, wie selten Sie einen Wert sogar veränderbar machen müssen. Immer wenn ich diese Sprache verwende, mache ich alles zu einer valund nur in sehr, sehr seltenen Fällen stelle ich fest, dass ich etwas in eine ändern muss var. 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.
KChaloux

22

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?

    public class Product
    {
        public string Name { get; set; }
    
        ...
    }
    
  • 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 angegeben nullsind, 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.


1
+1, insbesondere für "Sie müssen dies in einem Konstruktor steuern und nur dort." IMO das ist ein riesiger Vorteil.
Giorgio

10
Ein weiterer Vorteil: Das Kopieren eines Objekts ist kostenlos. Nur ein Hinweis.
Robert Grant

1
@MainMa Vielen Dank für Ihre Antwort, aber soweit ich weiß, gibt ReadOnlyDictionary keine Garantie dafür, dass jemand anderes das zugrunde liegende Wörterbuch nicht ändert (auch ohne Parallelität möchte ich möglicherweise den Verweis auf das ursprüngliche Wörterbuch innerhalb des Objekts speichern, zu dem die Methode gehört zur späteren Verwendung). ReadOnlyDictionary wird sogar in einem merkwürdigen Namespace deklariert: System.Collections.ObjectModel.
Den

2
@Den: Das bezieht sich auf eines meiner Lieblingsprobleme: Leute, die "schreibgeschützt" und "unveränderlich" als Synonyme betrachten. Wenn ein Objekt in einem Nur-Lese-Wrapper eingekapselt ist und kein anderer Verweis vorhanden ist oder irgendwo im Universum beibehalten wird, wird es durch das Umschließen des Objekts unveränderlich, und ein Verweis auf den Wrapper kann als Kurzform verwendet werden, um den Zustand des zu kapseln darin enthaltenes Objekt. Es gibt jedoch keinen Mechanismus, mit dem der Code feststellen kann, ob dies der Fall ist. Im Gegenteil, weil der Wrapper den Typ des
umschlossenen

2
... wird es dem Code unmöglich machen zu wissen, ob der resultierende Wrapper sicher als unveränderlich angesehen werden kann.
Supercat

13

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:

  1. B ist unveränderlich, kein Problem.
  2. B ist veränderlich, du machst eine defensive Kopie und gibst sie zurück. Leistungseinbußen, aber kein Risiko.
  3. B ist veränderlich, du gibst es zurück.

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.


9

Unveränderlichkeit kann auch die Implementierung von Abfallsammlern erheblich vereinfachen. Aus dem Wiki von GHC :

[...] Die Unveränderlichkeit von Daten zwingt uns dazu, viele temporäre Daten zu erstellen, aber es hilft auch, diesen Müll schnell zu sammeln. Der Trick ist, dass unveränderliche Daten NIE auf jüngere Werte verweisen. Tatsächlich existieren zum Zeitpunkt der Erstellung eines alten Wertes noch keine jüngeren Werte, sodass nicht von Grund auf darauf hingewiesen werden kann. Und da Werte niemals geändert werden, kann auch später nicht darauf hingewiesen werden. Dies ist die Schlüsseleigenschaft unveränderlicher Daten.

Dies vereinfacht die Garbage Collection (GC) erheblich. Wir können jederzeit die zuletzt erstellten Werte scannen und diejenigen, auf die nicht verwiesen wird, aus derselben Menge entfernen (natürlich sind echte Wurzeln der Hierarchie der aktiven Werte im Stapel vorhanden). [...] Es hat also ein kontraintuitives Verhalten: Je größer der Prozentsatz Ihrer Werte ist, desto schneller funktioniert es. [...]


5

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.xdasselbe 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 ?

  • Reduzierung der Speicherbelegung / frei?
  • Von Natur aus veränderlich? (zB Zähler)
  • Spart Modifikatoren, horizontales Rauschen? (const / final)
  • Verkürzt / vereinfacht sich Code? (init default, eventuell überschreiben nach)

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.


3

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 StringObjekte 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


1

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 Georgehat ein Feld vom Typ char[] chars;. Dieses Feld kann eine Zeichenfolge enthalten in:

  1. 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.

  2. Ein Array, zu dem keine externen Referenzen existieren, das George jedoch frei ändern kann.

  3. 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 charsGeorge derzeit die Zeichenfolge [Wind] charseinkapselt 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 charsin ein a.

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 Zeichenfolge Stringeinschließt, den Typ verwendet , der eine unveränderliche Zeichenfolge einschließt, gehen alle oben genannten Probleme verloren. Alle Felder des Typs Stringkapseln eine Folge von Zeichen mit einem gemeinsam nutzbaren Objekt, das sich nie ändern wird. Folglich, wenn ein Feld vom TypStringVerkapselt "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 von char[]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.


Es sieht nach einer vernünftigen Antwort aus, ist aber etwas schwer zu lesen und zu verstehen.
Den

1

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:

Bildbeschreibung hier eingeben

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 andzusammen 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:

on user operation:
    copy entire application state to undo entry
    perform operation

on undo/redo:
    swap application state with undo entry

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

Bildbeschreibung hier eingeben

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.


0

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:

  1. Snapshot-Semantik, mit der Sie Ihre Sammlungen auf eine Weise freigeben können, auf die sich der Empfänger verlassen kann, dass sie sich nie ändert.

  2. Implizite Thread-Sicherheit in Multithread-Anwendungen (für den Zugriff auf Sammlungen sind keine Sperren erforderlich).

  3. Jedes Mal, wenn Sie ein Klassenmitglied haben, das einen Auflistungstyp akzeptiert oder zurückgibt, und Sie schreibgeschützte Semantik in den Vertrag aufnehmen möchten.

  4. Funktionsprogrammierung freundlich.

  5. Ermöglichen Sie das Ändern einer Sammlung während der Aufzählung, während sichergestellt wird, dass sich die ursprüngliche Sammlung nicht ändert.

  6. Sie implementieren dieselben IReadOnly * -Schnittstellen, mit denen sich Ihr Code bereits befasst, sodass die Migration einfach ist.

Wenn Ihnen jemand eine ReadOnlyCollection, eine IReadOnlyList oder eine IEnumerable übergibt, besteht die einzige Garantie darin, dass Sie die Daten nicht ändern können - es gibt keine Garantie dafür, dass die Person, die Ihnen die Sammlung übergeben hat, sie nicht ändert. Dennoch braucht man oft ein gewisses Vertrauen, dass es sich nicht ändert. Diese Typen bieten keine Ereignisse an, mit denen Sie benachrichtigt werden, wenn sich ihr Inhalt ändert. Tritt dies möglicherweise in einem anderen Thread auf, möglicherweise während Sie den Inhalt auflisten? Ein solches Verhalten würde zur Beschädigung von Daten und / oder zu zufälligen Ausnahmen in Ihrer Anwendung führen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.