Veränderliche gegen unveränderliche Objekte


172

Ich versuche, mich mit veränderlichen und unveränderlichen Objekten zu beschäftigen. Die Verwendung von veränderlichen Objekten führt zu viel Druck (z. B. Rückgabe einer Reihe von Zeichenfolgen aus einer Methode), aber ich habe Probleme zu verstehen, welche negativen Auswirkungen dies hat. Was sind die Best Practices für die Verwendung veränderlicher Objekte? Sollten Sie sie nach Möglichkeit vermeiden?


stringist unveränderlich, zumindest in .NET, und ich denke auch in vielen anderen modernen Sprachen.
Domenic

4
es hängt davon ab, was ein String wirklich in der Sprache ist - erlang 'Strings' sind nur ein Array von Ints, und Hashkell- "Strings" sind Arrays von Zeichen.
Chii

4
Ruby-Strings sind ein veränderbares Array von Bytes. Macht mich absolut verrückt, dass Sie sie an Ort und Stelle ändern können.
Daniel Spiewak

6
Daniel - warum? Tun Sie es aus Versehen?

5
@DanielSpiewak Die Lösung für getriebene Muttern, mit denen Sie die Saiten an Ort und Stelle ändern können, ist einfach: Tun Sie es einfach nicht. Die Lösung, um verrückt zu werden, weil man die Saiten nicht an Ort und Stelle wechseln kann, ist nicht so einfach.
Kaz

Antworten:


162

Nun, das hat einige Aspekte.

  1. Veränderbare Objekte ohne Referenzidentität können zu ungeraden Zeiten Fehler verursachen. Betrachten Sie beispielsweise eine PersonBean mit einer wertbasierten equalsMethode:

    Map<Person, String> map = ...
    Person p = new Person();
    map.put(p, "Hey, there!");
    
    p.setName("Daniel");
    map.get(p);       // => null
    

    Die PersonInstanz geht in der Karte "verloren", wenn sie als Schlüssel verwendet wird, da ihre hashCodeund ihre Gleichheit auf veränderlichen Werten basieren. Diese Werte haben sich außerhalb der Karte geändert und das gesamte Hashing ist veraltet. Theoretiker mögen es, in diesem Punkt zu harfen, aber in der Praxis habe ich es nicht als zu großes Problem empfunden.

  2. Ein weiterer Aspekt ist die logische "Vernünftigkeit" Ihres Codes. Dies ist ein schwer zu definierender Begriff, der alles von der Lesbarkeit bis zum Ablauf umfasst. Generell sollten Sie in der Lage sein, einen Code zu betrachten und leicht zu verstehen, was er tut. Wichtiger ist jedoch, dass Sie sich davon überzeugen können, dass es das tut, was es richtig macht . Wenn sich Objekte unabhängig voneinander über verschiedene Code- "Domänen" hinweg ändern können, wird es manchmal schwierig, den Überblick darüber zu behalten, was wo und warum ist (" gruselige Fernwirkung "). Es ist schwieriger, dieses Konzept zu veranschaulichen, aber es ist etwas, mit dem größere, komplexere Architekturen häufig konfrontiert sind.

  3. Schließlich veränderbare Objekte sind Killer in konkurrierenden Situationen. Immer wenn Sie von separaten Threads aus auf ein veränderliches Objekt zugreifen, müssen Sie sich mit dem Sperren befassen. Dies reduziert den Durchsatz und erschwert die Wartung Ihres Codes erheblich . Ein ausreichend kompliziertes System lässt dieses Problem so weit überproportional werden, dass es (selbst für Parallelitätsexperten) nahezu unmöglich zu warten ist.

Unveränderliche Objekte (und insbesondere unveränderliche Sammlungen) vermeiden all diese Probleme. Sobald Sie sich ein Bild davon gemacht haben, wie sie funktionieren, entwickelt sich Ihr Code zu etwas, das leichter zu lesen, leichter zu warten und weniger wahrscheinlich auf seltsame und unvorhersehbare Weise ausfällt. Unveränderliche Objekte sind noch einfacher zu testen, nicht nur aufgrund ihrer einfachen Verspottbarkeit, sondern auch aufgrund der Codemuster, die sie tendenziell erzwingen. Kurz gesagt, sie sind überall eine gute Übung!

Trotzdem bin ich in dieser Angelegenheit kaum ein Eiferer. Einige Probleme lassen sich einfach nicht gut modellieren, wenn alles unveränderlich ist. Ich denke jedoch, dass Sie versuchen sollten, so viel Code wie möglich in diese Richtung zu verschieben, vorausgesetzt natürlich, Sie verwenden eine Sprache, die dies zu einer haltbaren Meinung macht (C / C ++ macht dies sehr schwierig, ebenso wie Java). . Kurz gesagt: Die Vorteile hängen etwas von Ihrem Problem ab, aber ich würde die Unveränderlichkeit eher bevorzugen.


11
Tolle Resonanz. Eine kleine Frage: Hat C ++ keine gute Unterstützung für Unveränderlichkeit? Ist die Konstantenkorrektheitsfunktion nicht ausreichend?
Dimitri C.

1
@DimitriC.: C ++ verfügt tatsächlich über einige grundlegendere Funktionen, insbesondere eine bessere Unterscheidung zwischen Speicherorten, die den nicht gemeinsam genutzten Status kapseln sollen, und solchen, die die Objektidentität kapseln.
Supercat

2
Im Falle der Java-Programmierung bringt Joshua Bloch in seinem berühmten Buch Effective Java (Punkt 15)
dMathieuD

27

Unveränderliche Objekte vs. unveränderliche Sammlungen

Einer der Feinheiten in der Debatte über veränderbare und unveränderliche Objekte ist die Möglichkeit, das Konzept der Unveränderlichkeit auf Sammlungen auszudehnen. Ein unveränderliches Objekt ist ein Objekt, das häufig eine einzelne logische Datenstruktur darstellt (z. B. eine unveränderliche Zeichenfolge). Wenn Sie einen Verweis auf ein unveränderliches Objekt haben, ändert sich der Inhalt des Objekts nicht.

Eine unveränderliche Sammlung ist eine Sammlung, die sich nie ändert.

Wenn ich eine Operation für eine veränderbare Sammlung ausführe, ändere ich die Sammlung an Ort und Stelle, und alle Entitäten, die Verweise auf die Sammlung haben, sehen die Änderung.

Wenn ich eine Operation für eine unveränderliche Sammlung ausführe, wird eine Referenz auf eine neue Sammlung zurückgegeben, die die Änderung widerspiegelt. Alle Entitäten, die Verweise auf frühere Versionen der Sammlung haben, sehen die Änderung nicht.

Clevere Implementierungen müssen nicht unbedingt die gesamte Sammlung kopieren (klonen), um diese Unveränderlichkeit zu gewährleisten. Das einfachste Beispiel ist der als einfach verknüpfte Liste implementierte Stapel und die Push / Pop-Operationen. Sie können alle Knoten aus der vorherigen Sammlung in der neuen Sammlung wiederverwenden, indem Sie nur einen einzigen Knoten für den Push hinzufügen und keine Knoten für den Pop klonen. Die push_tail-Operation für eine einfach verknüpfte Liste ist dagegen nicht so einfach oder effizient.

Unveränderliche vs. veränderbare Variablen / Referenzen

Einige funktionale Sprachen verwenden das Konzept der Unveränderlichkeit für Objektreferenzen selbst und erlauben nur eine einzige Referenzzuweisung.

  • In Erlang gilt dies für alle "Variablen". Ich kann einer Referenz nur einmal Objekte zuweisen. Wenn ich eine Sammlung bearbeiten würde, könnte ich die neue Sammlung nicht der alten Referenz (Variablenname) zuweisen.
  • Scala baut dies auch in die Sprache ein, wobei alle Referenzen mit var oder val deklariert werden , wobei vals nur eine einzelne Zuweisung sind und einen funktionalen Stil fördern, vars jedoch eine C-ähnliche oder Java-ähnliche Programmstruktur ermöglichen.
  • Die var / val-Deklaration ist erforderlich, während viele traditionelle Sprachen optionale Modifikatoren wie final in Java und const in C verwenden.

Einfache Entwicklung vs. Leistung

Fast immer besteht der Grund für die Verwendung eines unveränderlichen Objekts darin, die nebenwirkungsfreie Programmierung und das einfache Denken über den Code zu fördern (insbesondere in einer Umgebung mit hoher Parallelität / Parallelität). Sie müssen sich keine Sorgen machen, dass die zugrunde liegenden Daten von einer anderen Entität geändert werden, wenn das Objekt unveränderlich ist.

Der Hauptnachteil ist die Leistung. Hier ist eine Zusammenfassung eines einfachen Tests, den ich in Java durchgeführt habe , um einige unveränderliche mit veränderlichen Objekten in einem Spielzeugproblem zu vergleichen.

Die Leistungsprobleme sind in vielen Anwendungen umstritten, aber nicht in allen, weshalb viele große numerische Pakete, wie die Numpy Array-Klasse in Python, direkte Aktualisierungen großer Arrays ermöglichen. Dies wäre wichtig für Anwendungsbereiche, die große Matrix- und Vektoroperationen verwenden. Diese großen datenparallelen und rechenintensiven Probleme erreichen eine große Beschleunigung, wenn sie an Ort und Stelle arbeiten.


11

Überprüfen Sie diesen Blog-Beitrag: http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . Es erklärt, warum unveränderliche Objekte besser als veränderlich sind. Zusamenfassend:

  • unveränderliche Objekte sind einfacher zu konstruieren, zu testen und zu verwenden
  • Wirklich unveränderliche Objekte sind immer threadsicher
  • Sie helfen, zeitliche Kopplungen zu vermeiden
  • ihre Verwendung ist nebenwirkungsfrei (keine defensiven Kopien)
  • Identitätsveränderlichkeitsproblem wird vermieden
  • Sie haben immer Fehler Atomizität
  • Sie sind viel einfacher zwischenzuspeichern

10

Unveränderliche Objekte sind ein sehr mächtiges Konzept. Sie entlasten den Versuch, Objekte / Variablen für alle Clients konsistent zu halten, erheblich.

Sie können sie für nicht polymorphe Objekte auf niedriger Ebene verwenden - wie eine CPoint-Klasse -, die hauptsächlich mit Wertesemantik verwendet werden.

Oder Sie können sie für polymorphe Schnittstellen auf hoher Ebene verwenden - wie eine IF-Funktion, die eine mathematische Funktion darstellt -, die ausschließlich für die Objektsemantik verwendet wird.

Größter Vorteil: Unveränderlichkeit + Objektsemantik + intelligente Zeiger machen den Besitz von Objekten zu einem Problem. Alle Clients des Objekts verfügen standardmäßig über eine eigene private Kopie. Implizit bedeutet dies auch deterministisches Verhalten bei gleichzeitiger Anwendung.

Nachteil: Bei Verwendung mit Objekten, die viele Daten enthalten, kann der Speicherverbrauch zu einem Problem werden. Eine Lösung hierfür könnte darin bestehen, Operationen an einem Objekt symbolisch zu halten und eine verzögerte Auswertung durchzuführen. Dies kann dann jedoch zu Ketten symbolischer Berechnungen führen, die die Leistung negativ beeinflussen können, wenn die Schnittstelle nicht für symbolische Operationen ausgelegt ist. In diesem Fall sollten Sie unbedingt vermeiden, große Speicherblöcke von einer Methode zurückzugeben. In Kombination mit verketteten symbolischen Operationen kann dies zu einem massiven Speicherverbrauch und Leistungseinbußen führen.

Unveränderliche Objekte sind definitiv meine primäre Denkweise über objektorientiertes Design, aber sie sind kein Dogma. Sie lösen viele Probleme für Clients von Objekten, schaffen aber auch viele, insbesondere für die Implementierer.


Ich glaube, ich habe Abschnitt 4 zum größten Vorteil missverstanden: Unveränderlichkeit + Objektsemantik + intelligente Zeiger machen den Besitz von Objekten zu einem "strittigen" Punkt. Also ist es umstritten? Ich denke, Sie verwenden "moot" falsch ... Da das Objekt als nächster Satz "deterministisches Verhalten" aus seinem "moot" (umstrittenen) Verhalten impliziert.
Benjamin

Sie haben Recht, ich habe 'moot' falsch verwendet. Betrachten Sie es als geändert :)
QBziZ

6

Sie sollten angeben, über welche Sprache Sie sprechen. Für einfache Sprachen wie C oder C ++ bevorzuge ich die Verwendung veränderbarer Objekte, um Platz zu sparen und die Speicherabwanderung zu verringern. In höheren Sprachen erleichtern unveränderliche Objekte das Nachdenken über das Verhalten des Codes (insbesondere Multithread-Code), da es keine "gruselige Fernwirkung" gibt.


Sie schlagen vor, dass die Fäden quantenverschränkt sind? Das ist eine ziemliche Strecke :) Die Fäden sind, wenn man darüber nachdenkt, tatsächlich ziemlich nahe daran, sich zu verwickeln. Eine Änderung, die ein Thread vornimmt, wirkt sich auf den anderen aus. +1
ndrewxie

4

Ein veränderliches Objekt ist einfach ein Objekt, das geändert werden kann, nachdem es erstellt / instanziiert wurde, im Vergleich zu einem unveränderlichen Objekt, das nicht geändert werden kann (siehe die Wikipedia-Seite zum Thema). Ein Beispiel hierfür in einer Programmiersprache sind Pythons-Listen und -Tupel. Listen können geändert werden (z. B. können nach dem Erstellen neue Elemente hinzugefügt werden), Tupel hingegen nicht.

Ich glaube nicht, dass es eine eindeutige Antwort gibt, welche für alle Situationen besser ist. Sie haben beide ihre Plätze.


1

Wenn ein Klassentyp veränderbar ist, kann eine Variable dieses Klassentyps verschiedene Bedeutungen haben. Angenommen, ein Objekt foohat ein Feld int[] arrund enthält einen Verweis auf ein Objekt int[3]mit den Zahlen {5, 7, 9}. Obwohl die Art des Feldes bekannt ist, gibt es mindestens vier verschiedene Dinge, die es darstellen kann:

  • Eine potentiell-shared Referenz, deren sämtliche Halter kümmert sich nur , dass sie die Werte 5, 7 und 9. Wenn kapselt fooBedürfnisse arrunterschiedliche Werte verkapseln, muss sie durch eine andere Anordnung ersetzt werden , die die gewünschten Werte enthält. Wenn man eine Kopie von erstellen möchte foo, kann man der Kopie entweder einen Verweis auf arroder ein neues Array mit den Werten {1,2,3} geben, je nachdem, was bequemer ist.

  • Der einzige Verweis irgendwo im Universum auf ein Array, das die Werte 5, 7 und 9 kapselt. Satz von drei Speicherorten, die im Moment die Werte 5, 7 und 9 enthalten. Wenn fooes die Werte 5, 8 und 9 kapseln soll, kann es entweder das zweite Element in diesem Array ändern oder ein neues Array mit den Werten 5, 8 und 9 erstellen und das alte verlassen. Beachten Sie, dass, wenn Sie eine Kopie von fooerstellen möchten, diese in der Kopie durch arreinen Verweis auf ein neues Array ersetzt werden muss foo.arr, um als einziger Verweis auf dieses Array irgendwo im Universum zu bleiben.

  • Ein Verweis auf ein Array, das einem anderen Objekt gehört, das es fooaus irgendeinem Grund ausgesetzt hat (z. B. möchte es möglicherweise fooeinige Daten dort speichern). In diesem Szenario wird arrder Inhalt des Arrays nicht gekapselt, sondern dessen Identität . Da das Ersetzen arrdurch einen Verweis auf ein neues Array seine Bedeutung vollständig ändern würde, sollte eine Kopie von fooeinen Verweis auf dasselbe Array enthalten.

  • Ein Verweis auf ein Array, foodessen alleiniger Eigentümer es ist, dessen Verweise jedoch aus irgendeinem Grund von einem anderen Objekt gehalten werden (z. B. möchte das andere Objekt Daten dort speichern - die Kehrseite des vorherigen Falls). In diesem Szenario werden arrsowohl die Identität des Arrays als auch sein Inhalt gekapselt. Das Ersetzen arrdurch einen Verweis auf ein neues Array würde seine Bedeutung völlig ändern, aber das arrVerweisen auf einen Klon foo.arrwürde die Annahme verletzen, dass fooder alleinige Eigentümer ist. Es gibt also keine Möglichkeit zum Kopieren foo.

Theoretisch int[]sollte es ein schöner einfacher, gut definierter Typ sein, aber er hat vier sehr unterschiedliche Bedeutungen. Im Gegensatz dazu hat eine Bezugnahme auf ein unveränderliches Objekt (z. B. String) im Allgemeinen nur eine Bedeutung. Ein Großteil der "Kraft" unveränderlicher Objekte beruht auf dieser Tatsache.


1

Veränderbare Instanzen werden als Referenz übergeben.

Unveränderliche Instanzen werden als Wert übergeben.

Abstraktes Beispiel. Angenommen, auf meiner Festplatte befindet sich eine Datei mit dem Namen txtfile . Wenn Sie mich jetzt um txtfile bitten , kann ich es in zwei Modi zurückgeben:

  1. Erstellen Sie eine Verknüpfung zu txtfile und pas Verknüpfung zu Ihnen, oder
  2. Nehmen Sie eine Kopie für txtfile und pas copy zu Ihnen.

Im ersten Modus ist die zurückgegebene txt- Datei eine veränderbare Datei, da Sie bei Änderungen an der Verknüpfungsdatei auch Änderungen an der Originaldatei vornehmen. Der Vorteil dieses Modus besteht darin, dass für jede zurückgegebene Verknüpfung weniger Speicher (im RAM oder auf der Festplatte) erforderlich ist. Der Nachteil besteht darin, dass jeder (nicht nur ich, der Eigentümer) die Berechtigung zum Ändern des Dateiinhalts hat.

Im zweiten Modus ist die zurückgegebene txt- Datei eine unveränderliche Datei, da sich alle Änderungen in der empfangenen Datei nicht auf die Originaldatei beziehen. Der Vorteil dieses Modus ist, dass nur ich (der Besitzer) die Originaldatei ändern kann. Der Nachteil ist, dass für jede zurückgegebene Kopie Speicher erforderlich ist (im RAM oder auf der Festplatte).


Dies ist nicht unbedingt richtig. Unveränderliche Instanzen können tatsächlich als Referenz übergeben werden und sind es häufig. Das Kopieren erfolgt hauptsächlich, wenn Sie das Objekt oder die Datenstruktur aktualisieren müssen.
Jamon Holmgren

0

Wenn Sie Referenzen eines Arrays oder einer Zeichenfolge zurückgeben, kann die Außenwelt den Inhalt in diesem Objekt ändern und ihn somit als veränderbares (veränderbares) Objekt festlegen.


0

Unveränderliche Mittel können nicht geändert werden, und veränderliche Mittel können geändert werden.

Objekte unterscheiden sich von Grundelementen in Java. Grundelemente sind in Typen (Boolean, Int usw.) erstellt, und Objekte (Klassen) sind vom Benutzer erstellte Typen.

Grundelemente und Objekte können veränderlich oder unveränderlich sein, wenn sie als Elementvariablen innerhalb der Implementierung einer Klasse definiert werden.

Viele Leute denken, dass Primitive und Objektvariablen mit einem letzten Modifikator unveränderlich sind, dies ist jedoch nicht genau richtig. Final bedeutet also fast nicht unveränderlich für Variablen. Siehe Beispiel hier
http://www.siteconsortium.com/h/D0000F.php .

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.