Ab wann werden unveränderliche Klassen zur Last?

16

Beim Entwerfen von Klassen, die Ihr Datenmodell enthalten sollen, kann es hilfreich sein, unveränderliche Objekte zu erstellen. Ab wann wird die Belastung durch Konstruktorparameterlisten und tiefe Kopien jedoch zu groß, und Sie müssen die unveränderliche Einschränkung aufgeben?

Zum Beispiel ist hier eine unveränderliche Klasse, um eine benannte Sache darzustellen (ich verwende C # -Syntax, aber das Prinzip gilt für alle OO-Sprachen)

class NamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    
    public NamedThing(NamedThing other)
    {
         this._name = other._name;
    }
    public string Name
    {
        get { return _name; }
    }
}

Benannte Objekte können erstellt, abgefragt und in neue benannte Objekte kopiert werden, der Name kann jedoch nicht geändert werden.

Das ist alles in Ordnung, aber was passiert, wenn ich ein weiteres Attribut hinzufügen möchte? Ich muss dem Konstruktor einen Parameter hinzufügen und den Kopierkonstruktor aktualisieren. Das ist nicht zu viel Arbeit, aber die Probleme beginnen, soweit ich sehen kann, wenn ich ein komplexes Objekt unveränderlich machen möchte .

Wenn die Klasse möglicherweise Attribute und Sammlungen enthält, die andere komplexe Klassen enthalten, scheint mir die Konstruktorparameterliste ein Albtraum zu werden.

Ab wann wird eine Klasse zu komplex , um unveränderlich zu sein?

Tony
quelle
Ich bemühe mich immer, die Klassen in meinem Modell unveränderlich zu machen. Wenn Sie große, lange Konstruktorparameterlisten haben, ist Ihre Klasse möglicherweise zu groß und kann aufgeteilt werden? Wenn Ihre untergeordneten Objekte auch unveränderlich sind und dem gleichen Muster folgen, sollten Ihre übergeordneten Objekte nicht (zu viel) leiden. Ich finde es VIEL schwieriger, eine vorhandene Klasse zu ändern, um unveränderlich zu werden, als ein Datenmodell unveränderlich zu machen, wenn ich von vorne anfange.
Niemand
1
Sie können sich das in dieser Frage vorgeschlagene Builder-Muster ansehen: stackoverflow.com/questions/1304154/…
Ant
Haben Sie sich MemberwiseClone angesehen? Sie müssen den Kopierkonstruktor nicht für jedes neue Mitglied aktualisieren.
Kevin Cline
3
@Tony Wenn Ihre Sammlungen und alles, was sie enthalten, auch unveränderlich sind, benötigen Sie keine tiefe Kopie, eine flache Kopie ist ausreichend.
mjcopple
2
Abgesehen davon verwende ich häufig "einmal festlegen" -Felder in Klassen, in denen die Klasse "ziemlich" unveränderlich, aber nicht vollständig sein muss. Ich finde, dies löst das Problem riesiger Konstruktoren, bietet aber die meisten Vorteile unveränderlicher Klassen. (Das heißt, Ihr interner Klassencode muss sich keine Sorgen machen, dass sich der Wert ändert)
Earlz

Antworten:

23

Wann werden sie zur Last? Sehr schnell (insbesondere, wenn Ihre Sprache keine ausreichende syntaktische Unterstützung für die Unveränderlichkeit bietet.)

Unveränderlichkeit wird als Königsweg für das Multicore-Dilemma und all das verkauft. Die Unveränderlichkeit in den meisten OO-Sprachen zwingt Sie jedoch dazu, künstliche Artefakte und Methoden in Ihr Modell und Ihren Prozess einzufügen. Für jede komplexe unveränderliche Klasse müssen Sie einen (zumindest intern) gleich komplexen Builder haben. Egal wie Sie es entwerfen, es führt immer noch eine starke Kopplung ein (daher haben wir einen guten Grund, sie einzuführen.)

Es ist nicht unbedingt möglich, alles in kleinen, nicht komplexen Klassen zu modellieren. Also für große Klassen und Strukturen, können wir sie künstlich aufzuteilen - nicht , weil das Gefühl in unserem Domain - Modell macht, sondern weil wir mit ihrer komplexen Instanziierung und Bauherr in Code zu tun haben.

Es ist noch schlimmer, wenn Menschen die Idee der Unveränderlichkeit in einer Allzwecksprache wie Java oder C # zu weit führen und alles unveränderlich machen. Infolgedessen werden Konstrukte mit s-Ausdrücken in Sprachen erzwungen, die solche Dinge nicht mühelos unterstützen.

Engineering ist der Akt der Modellierung durch Kompromisse und Kompromisse. Alles durch Erlass unveränderlich zu machen, weil jemand liest, dass alles in der funktionalen Sprache X oder Y (einem völlig anderen Programmiermodell) unveränderlich ist, ist nicht akzeptabel. Das ist keine gute Technik.

Kleine, möglicherweise einheitliche Dinge können unveränderlich gemacht werden. Komplexere Dinge können unveränderlich gemacht werden, wenn es Sinn macht . Aber Unveränderlichkeit ist keine Wunderwaffe. Die Fähigkeit, Fehler zu reduzieren, die Skalierbarkeit und Leistung zu verbessern, ist nicht die einzige Funktion der Unveränderlichkeit. Es ist eine Funktion der richtigen Ingenieurpraxis . Schließlich haben die Leute gute, skalierbare Software ohne Unveränderlichkeit geschrieben.

Unveränderlichkeit wird sehr schnell zu einer Last (sie erhöht die zufällige Komplexität), wenn sie ohne Grund ausgeführt wird, wenn sie außerhalb dessen erfolgt, was im Kontext eines Domänenmodells sinnvoll ist.

Zum einen versuche ich es zu vermeiden (es sei denn, ich arbeite in einer Programmiersprache mit guter syntaktischer Unterstützung).

luis.espinal
quelle
6
Luis, haben Sie bemerkt, dass die gut geschriebenen, pragmatisch korrekten Antworten, die mit einer Erklärung einfacher, aber solider technischer Prinzipien verfasst wurden, tendenziell nicht so viele Stimmen erhalten wie diejenigen, die moderne Codierungsfads verwenden? Dies ist eine großartige Antwort.
Huperniketes
3
Danke :) Ich habe die Wiederholungstrends selbst bemerkt, aber das ist in Ordnung. Fad Fanboys Churn-Code, den wir später zu besseren Stundensätzen reparieren können, hahah :) jk ...
luis.espinal
4
Silberkugel? Nein. Lohnt sich ein bisschen Unbeholfenheit in C # / Java (es ist nicht wirklich so schlimm)? Absolut. Auch die Rolle von Multicores bei der Unveränderlichkeit spielt eine untergeordnete Rolle ... der eigentliche Vorteil ist die einfache Argumentation.
Mauricio Scheffer
@ Mauricio - wenn Sie so sagen (dass Unveränderlichkeit in Java nicht so schlecht ist). Nachdem ich von 1998 bis 2011 an Java gearbeitet habe, möchte ich sagen, dass es in einfachen Codebasen keine Kleinigkeit ist. Menschen haben jedoch unterschiedliche Erfahrungen, und ich gebe zu, dass mein POV nicht frei von Subjektivität ist. Ich kann dem leider nicht zustimmen. Ich stimme jedoch zu, dass die Leichtigkeit des Denkens das wichtigste ist, wenn es um Unveränderlichkeit geht.
Luis.espinal
13

Ich habe eine Phase durchlaufen, in der darauf bestanden wurde, dass der Unterricht, wo immer möglich, unveränderlich ist. Hatte Builder für so ziemlich alles, unveränderliche Arrays usw. Ich fand die Antwort auf Ihre Frage einfach: Ab wann werden unveränderliche Klassen zur Last? Sehr schnell. Sobald Sie etwas serialisieren möchten, müssen Sie in der Lage sein, es zu deserialisieren, was bedeutet, dass es veränderlich sein muss. Sobald Sie ein ORM verwenden möchten, bestehen die meisten von ihnen darauf, dass Eigenschaften veränderbar sind. Und so weiter.

Ich habe diese Richtlinie schließlich durch unveränderliche Schnittstellen zu veränderlichen Objekten ersetzt.

class NamedThing : INamedThing
{
    private string _name;    
    public NamedThing(string name)
    {
        _name = name;
    }    

    public NamedThing(NamedThing other)
    {
        this._name = other._name;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

interface INamedThing
{
    string Name { get; }
}

Jetzt ist das Objekt flexibel, aber Sie können dem aufrufenden Code immer noch mitteilen, dass diese Eigenschaften nicht bearbeitet werden sollen.

pdr
quelle
8
Abgesehen von kleinen Unstimmigkeiten stimme ich zu, dass unveränderliche Objekte beim Programmieren in einer imperativen Sprache sehr schnell zu einem Ärgernis werden können, aber ich bin mir nicht sicher, ob eine unveränderliche Benutzeroberfläche das gleiche Problem oder überhaupt ein Problem löst. Der Hauptgrund für die Verwendung eines unveränderlichen Objekts ist, dass Sie es jederzeit und überall herumwerfen können und sich niemals Sorgen machen müssen, dass jemand anderes Ihren Zustand verfälscht. Wenn das zugrunde liegende Objekt veränderbar ist, haben Sie diese Garantie nicht, insbesondere, wenn Sie es veränderbar gehalten haben, weil verschiedene Dinge es verändern müssen.
Aaronaught
1
@Aaronaught - Interessanter Punkt. Ich denke, es ist mehr eine psychologische Sache als ein tatsächlicher Schutz. Ihre letzte Zeile hat jedoch eine falsche Prämisse. Der Grund dafür, dass es veränderlich bleibt, ist mehr, dass verschiedene Dinge es instanziieren und durch Reflektion besiedeln müssen, um es nicht zu mutieren, wenn es einmal instanziiert ist.
pdr
1
@Aaronaught: Der gleiche Weg IComparable<T>garantiert, dass wenn X.CompareTo(Y)>0und Y.CompareTo(Z)>0dann X.CompareTo(Z)>0. Schnittstellen haben Verträge . Wenn im Vertrag für IImmutableList<T>festgelegt ist, dass die Werte aller Elemente und Eigenschaften "in Stein gemeißelt" werden müssen, bevor eine Instanz der Außenwelt ausgesetzt wird, werden alle legitimen Implementierungen dies tun. Nichts hindert eine IComparableImplementierung daran, die Transitivität zu verletzen, aber Implementierungen, die dies tun, sind unzulässig. Wenn eine SortedDictionaryFehlfunktion bei einer unehelichen IComparable, ...
Supercat
1
@Aaronaught: Warum soll ich darauf vertrauen , dass Implementierungen von IReadOnlyList<T>unveränderlich sein wird, da (1) keine solche Anforderung in der Interface - Dokumentation angegeben wird, und (2) die häufigste Implementierung List<T>, ist nicht einmal schreibgeschützt ? Mir ist nicht ganz klar, was an meinen Begriffen nicht eindeutig ist: Eine Sammlung ist lesbar, wenn die darin enthaltenen Daten gelesen werden können. Es ist schreibgeschützt, wenn versprochen werden kann, dass die enthaltenen Daten nur geändert werden können, wenn ein externer Verweis im Code enthalten ist, der dies ändern würde. Es ist unveränderlich, wenn es garantieren kann, dass es nicht geändert werden kann, Punkt.
Supercat
1
@supercat: Microsoft stimmt mir übrigens zu. Sie haben ein unveränderliches Auflistungspaket veröffentlicht und festgestellt, dass es sich um konkrete Typen handelt, da Sie niemals garantieren können, dass eine abstrakte Klasse oder Schnittstelle wirklich unveränderlich ist.
Aaronaught
8

Ich glaube nicht, dass es eine allgemeine Antwort darauf gibt. Je komplexer eine Klasse ist, desto schwieriger ist es, über ihre Statusänderungen nachzudenken, und desto teurer ist es, neue Kopien davon zu erstellen. Ab einer gewissen (persönlichen) Komplexität wird es zu schmerzhaft, eine Klasse unveränderlich zu machen / zu halten.

Beachten Sie, dass eine zu komplexe Klasse oder eine lange Liste von Methodenparametern Designgerüche an sich sind, unabhängig von der Unveränderlichkeit.

Daher besteht die bevorzugte Lösung normalerweise darin, eine solche Klasse in mehrere unterschiedliche Klassen zu unterteilen, von denen jede für sich veränderlich oder unveränderlich gemacht werden kann. Wenn dies nicht möglich ist, kann es wandelbar gemacht werden.

Péter Török
quelle
5

Sie können das Kopierproblem vermeiden, wenn Sie alle Ihre unveränderlichen Felder in einem inneren speichern struct. Dies ist im Grunde eine Variation des Erinnerungsmusters. Wenn Sie dann eine Kopie erstellen möchten, kopieren Sie einfach das Erinnerungsstück:

class MyClass
{
    struct Memento
    {
        public int field1;
        public string field2;
    }

    private readonly Memento memento;

    public MyClass(int field1, string field2)
    {
        this.memento = new Memento()
            {
                field1 = field1,
                field2 = field2
            };
    }

    private MyClass(Memento memento) // for copying
    {
        this.memento = memento;
    }

    public int Field1 { get { return this.memento.field1; } }
    public string Field2 { get { return this.memento.field2; } }

    public MyClass WithNewField1(int newField1)
    {
        Memento newMemento = this.memento;
        newMemento.field1 = newField1;
        return new MyClass(newMemento);
    }
}
Scott Whitlock
quelle
Ich denke, die innere Struktur ist nicht notwendig. Es ist nur eine andere Art, MemberwiseClone zu machen.
Codism
@Codism - ja und nein. Es kann vorkommen, dass Sie andere Mitglieder benötigen, die Sie nicht klonen möchten. Was wäre, wenn Sie in einem Ihrer Getter Lazy Evaluation verwenden und das Ergebnis in einem Mitglied zwischenspeichern würden? Wenn Sie einen MemberwiseClone ausführen, klonen Sie den zwischengespeicherten Wert und ändern dann eines Ihrer Mitglieder, von dem der zwischengespeicherte Wert abhängt. Es ist sauberer, den Status vom Cache zu trennen.
Scott Whitlock
Es könnte erwähnenswert sein, einen weiteren Vorteil der inneren Struktur zu erwähnen: Sie erleichtert es einem Objekt, seinen Zustand auf ein Objekt zu kopieren, auf das andere Referenzen existieren . Eine häufige Ursache für Unklarheiten in OOP ist, ob eine Methode, die eine Objektreferenz zurückgibt, eine Ansicht eines Objekts zurückgibt, die sich möglicherweise außerhalb der Kontrolle des Empfängers ändert. Wenn eine Methode keine Objektreferenz zurückgibt, sondern eine Objektreferenz vom Aufrufer akzeptiert und den Status in diese kopiert, wird das Eigentum am Objekt viel klarer. Ein solcher Ansatz funktioniert nicht gut mit frei vererbbaren Typen, aber ...
Supercat
... kann es bei veränderlichen Datenhaltern sehr nützlich sein. Der Ansatz macht es auch sehr einfach, "parallele" veränderbare und unveränderbare Klassen (abgeleitet von einer abstrakten "lesbaren" Basis) zu haben und ihre Konstruktoren in der Lage zu sein, Daten voneinander zu kopieren.
Supercat
3

Hier arbeiten ein paar Dinge. Unveränderliche Datensätze eignen sich hervorragend für die Multithread-Skalierbarkeit. Grundsätzlich können Sie Ihren Speicher erheblich optimieren, sodass ein Parametersatz eine Instanz der Klasse ist - überall. Da sich die Objekte nie ändern, müssen Sie sich nicht um die Synchronisierung kümmern, wenn Sie auf ihre Mitglieder zugreifen. Das ist gut. Wie Sie jedoch betonen, benötigen Sie eine gewisse Veränderlichkeit, je komplexer das Objekt ist. Ich würde mit folgenden Überlegungen beginnen:

  • Gibt es einen geschäftlichen Grund, warum ein Objekt seinen Zustand ändern kann ? Beispielsweise ist ein in einer Datenbank gespeichertes Benutzerobjekt aufgrund seiner ID eindeutig, es muss jedoch in der Lage sein, den Status im Laufe der Zeit zu ändern. Wenn Sie hingegen die Koordinaten in einem Gitter ändern, ist dies nicht mehr die ursprüngliche Koordinate. Daher ist es sinnvoll, die Koordinaten unveränderlich zu machen. Gleiches gilt für Streicher.
  • Können einige der Attribute berechnet werden? Kurz gesagt, wenn die anderen Werte in der neuen Kopie eines Objekts eine Funktion eines Kernwerts sind, den Sie übergeben, können Sie sie entweder im Konstruktor oder bei Bedarf berechnen. Dies reduziert den Wartungsaufwand, da Sie diese Werte beim Kopieren oder Erstellen auf dieselbe Weise initialisieren können.
  • Wie viele Werte bilden das neue unveränderliche Objekt? Irgendwann wird die Komplexität des Erstellens eines Objekts nicht mehr trivial, und an diesem Punkt können mehrere Instanzen des Objekts zu einem Problem werden. Beispiele hierfür sind unveränderliche Baumstrukturen, Objekte mit mehr als drei übergebenen Parametern usw. Je mehr Parameter vorhanden sind, desto größer ist die Möglichkeit, die Reihenfolge der Parameter zu verfälschen oder die falsche aufzuheben.

In Sprachen, die nur unveränderliche Objekte unterstützen (z. B. Erlang), ist das Endergebnis eine neue Kopie des Objekts mit dem aktualisierten Wert, wenn eine Operation den Status eines unveränderlichen Objekts zu ändern scheint. Wenn Sie beispielsweise einen Artikel zu einem Vektor / einer Liste hinzufügen, gehen Sie wie folgt vor:

myList = lists:append([[1,2,3], [4,5,6]])
% myList is now [1,2,3,4,5,6]

Das kann eine vernünftige Art sein, mit komplizierteren Objekten zu arbeiten. Wenn Sie beispielsweise einen Baumknoten hinzufügen, ist das Ergebnis ein neuer Baum mit dem hinzugefügten Knoten. Die Methode im obigen Beispiel gibt eine neue Liste zurück. In dem Beispiel in diesem Absatz wird tree.add(newNode)ein neuer Baum mit dem hinzugefügten Knoten zurückgegeben. Für die Benutzer wird es einfach, damit zu arbeiten. Für die Bibliotheksschreiber wird es langweilig, wenn die Sprache das implizite Kopieren nicht unterstützt. Diese Schwelle liegt in Ihrer eigenen Geduld. Für die Benutzer Ihrer Bibliothek liegt das vernünftigste Limit, das ich gefunden habe, bei drei bis vier Parameterobergrenzen.

Berin Loritsch
quelle
Wenn man geneigt wäre, einen veränderlichen Objektverweis als Wert zu verwenden [dh, dass keine Verweise in seinem Eigentümer enthalten und niemals verfügbar sind], ist das Konstruieren eines neuen unveränderlichen Objekts, das den gewünschten "geänderten" Inhalt enthält, gleichbedeutend mit dem direkten Ändern des Objekts. obwohl ist wahrscheinlich langsamer. Veränderbare Objekte können jedoch auch als Entitäten verwendet werden . Wie würde man Dinge machen, die sich wie Entitäten verhalten, ohne veränderbare Objekte?
Supercat
0

Wenn Sie mehrere Endklassenmitglieder haben und nicht möchten, dass sie allen Objekten ausgesetzt werden, die sie erstellen müssen, können Sie das Builder-Muster verwenden:

class NamedThing
{
    private string _name;    
    private string _value;
    private NamedThing(string name, string value)
    {
        _name = name;
        _value = value;
    }    
    public NamedThing(NamedThing other)
    {
        this._name = other._name;
        this._value = other._value;
    }
    public string Name
    {
        get { return _name; }
    }

    public static class Builder {
        string _name;
        string _value;

        public void setValue(string value) {
            _value = value;
        }
        public void setName(string name) {
            _name = name;
        }
        public NamedThing newObject() {
            return new NamedThing(_name, _value);
        }
    }
}

Der Vorteil ist, dass Sie auf einfache Weise ein neues Objekt mit nur einem anderen Wert eines anderen Namens erstellen können.

Salandur
quelle
Ich denke, dass Ihr Builder als statisch nicht korrekt ist. Ein anderer Thread könnte den statischen Namen oder den statischen Wert ändern, nachdem Sie diese festgelegt haben, aber bevor Sie aufrufen newObject.
ErikE
Nur die Builder-Klasse ist statisch, ihre Mitglieder jedoch nicht. Dies bedeutet, dass für jeden von Ihnen erstellten Builder eine eigene Gruppe von Elementen mit entsprechenden Werten vorhanden ist. Die Klasse muss statisch sein, damit sie außerhalb der enthaltenden Klasse verwendet und instanziiert werden kann ( NamedThingin diesem Fall)
Salandur
Ich sehe, was Sie sagen, ich stelle mir nur ein Problem damit vor, weil es einen Entwickler nicht dazu bringt, "in die Grube des Erfolgs zu fallen". Die Tatsache, dass statische Variablen verwendet werden, bedeutet, dass bei einer BuilderWiederverwendung von a das von mir erwähnte Risiko besteht, dass etwas passiert. Jemand erstellt möglicherweise viele Objekte und entscheidet, dass die meisten Eigenschaften identisch sind, um sie einfach wiederzuverwenden. BuilderMachen wir es also zu einem globalen Singleton, dem Abhängigkeiten hinzugefügt werden! Hoppla. Major Bugs eingeführt. Ich denke also, dass dieses Muster von gemischt instanziierten vs. statischen Mustern schlecht ist.
ErikE
1
@Salandur Innere Klassen in C # sind im Sinne einer inneren Java-Klasse immer "statisch".
Sebastian Redl
0

Ab wann wird eine Klasse zu komplex, um unveränderlich zu sein?

Meiner Meinung nach lohnt es sich nicht, kleine Klassen in Sprachen wie der von Ihnen gezeigten unveränderlich zu machen . Ich benutze hier kleine und nicht komplexe , denn selbst wenn Sie dieser Klasse zehn Felder hinzufügen und damit wirklich ausgefallene Operationen ausführen, bezweifle ich, dass es Kilobyte oder Megabyte oder Gigabyte dauern wird class kann einfach eine billige Kopie des gesamten Objekts erstellen, um das Original nicht zu verändern, wenn es keine externen Nebenwirkungen verursachen soll.

Persistente Datenstrukturen

Ich persönlich verwende die Unveränderlichkeit für große, zentrale Datenstrukturen, die eine Reihe von wichtigen Daten wie Instanzen der Klasse, die Sie anzeigen, wie eine, die eine Million speichert, zusammenfassen NamedThings. Durch die Zugehörigkeit zu einer persistenten Datenstruktur, die unveränderlich ist und sich hinter einer Schnittstelle befindet, die nur Lesezugriff erlaubt, werden die zum Container gehörenden Elemente unveränderlich, ohne dass die Elementklasse ( NamedThing) damit umgehen muss.

Günstige Kopien

Die persistente Datenstruktur ermöglicht es, Bereiche davon zu transformieren und eindeutig zu machen, wodurch Änderungen am Original vermieden werden, ohne dass die Datenstruktur in ihrer Gesamtheit kopiert werden muss. Das ist das Schöne daran. Wenn Sie naiv Funktionen schreiben möchten, die Nebenwirkungen vermeiden, die eine Datenstruktur eingeben, die Gigabyte Speicherplatz beansprucht und nur einen Megabyte-Speicherplatz verändert, müssen Sie das ganze Freaking-Ding kopieren, um zu vermeiden, die Eingabe zu berühren und ein neues zurückzugeben Ausgabe. Entweder kopieren Sie Gigabyte, um Nebenwirkungen zu vermeiden, oder verursachen Nebenwirkungen in diesem Szenario, sodass Sie zwischen zwei unangenehmen Optionen wählen müssen.

Mit einer dauerhaften Datenstruktur können Sie eine solche Funktion schreiben und vermeiden, dass eine Kopie der gesamten Datenstruktur erstellt wird. Die Ausgabe erfordert nur etwa ein Megabyte zusätzlichen Speicher, wenn Ihre Funktion nur den Speicher eines Megabytes transformiert.

Belastung

Was die Bürde angeht, so gibt es zumindest in meinem Fall eine unmittelbare. Ich brauche diese Builder, von denen die Leute sprechen oder "Transienten", wie ich sie nenne, um Transformationen in diese massive Datenstruktur effektiv ausdrücken zu können, ohne sie zu berühren. Code wie folgt:

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
     // Transform stuff in the range, [first, last).
     for (; first != last; ++first)
          transform(stuff[first]);
}

... muss dann so geschrieben werden:

ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
     // Grab a "transient" (builder) list we can modify:
     TransientList<Stuff> transient(stuff);

     // Transform stuff in the range, [first, last)
     // for the transient list.
     for (; first != last; ++first)
          transform(transient[first]);

     // Commit the modifications to get and return a new
     // immutable list.
     return stuff.commit(transient);
}

Im Gegenzug zu diesen beiden zusätzlichen Codezeilen kann die Funktion jetzt sicher über Threads mit derselben ursprünglichen Liste aufgerufen werden, verursacht keine Nebenwirkungen usw. Außerdem ist es sehr einfach, diesen Vorgang zu einer nicht rückgängig zu machenden Benutzeraktion zu machen, da die Undo kann nur eine billige, flache Kopie der alten Liste speichern.

Ausnahmesicherheit oder Fehlerbehebung

Nicht jeder profitiert in solchen Kontexten so sehr von persistenten Datenstrukturen wie ich (ich habe sie in Undo-Systemen und bei der zerstörungsfreien Bearbeitung, die zentrale Begriffe in meiner VFX-Domäne sind, so häufig verwendet), aber eine Sache, die für ungefähr gilt Jeder, der berücksichtigt werden muss, ist Ausnahmesicherheit oder Fehlerbehebung .

Wenn Sie die ursprüngliche Mutationsfunktion ausnahmesicher machen möchten, ist eine Rollback-Logik erforderlich, für die die einfachste Implementierung das Kopieren der gesamten Liste erfordert :

void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
    // Make a copy of the whole massive gigabyte-sized list 
    // in case we encounter an exception and need to rollback
    // changes.
    MutList<Stuff> old_stuff = stuff;

    try
    {
         // Transform stuff in the range, [first, last).
         for (; first != last; ++first)
             transform(stuff[first]);
    }
    catch (...)
    {
         // If the operation failed and ran into an exception,
         // swap the original list with the one we modified
         // to "undo" our changes.
         stuff.swap(old_stuff);
         throw;
    }
}

Zu diesem Zeitpunkt ist die ausnahmesichere veränderbare Version noch rechenintensiver und wahrscheinlich noch schwieriger zu schreiben als die unveränderliche Version, die einen "Builder" verwendet. Und viele C ++ - Entwickler vernachlässigen die Ausnahmesicherheit einfach und das ist vielleicht in Ordnung für ihre Domain, aber in meinem Fall möchte ich sicherstellen, dass mein Code auch im Falle einer Ausnahme korrekt funktioniert (selbst wenn ich Tests schreibe, die absichtlich Ausnahmen auslösen, um Ausnahmen zu testen Sicherheit), und das macht es so, dass ich in der Lage sein muss, alle Nebenwirkungen, die eine Funktion hervorruft, rückgängig zu machen, wenn etwas auslöst.

Wenn Sie ausnahmesicher sein und Fehler ordnungsgemäß beheben möchten, ohne dass Ihre Anwendung abstürzt und brennt, müssen Sie alle Nebenwirkungen, die eine Funktion im Falle eines Fehlers / einer Ausnahme verursachen kann, rückgängig machen. Und da kann der Builder tatsächlich mehr Programmiererzeit sparen, als es kostet, zusammen mit Rechenzeit, weil:

In einer Funktion, die keine Nebenwirkungen verursacht, müssen Sie sich keine Gedanken mehr über das Zurücksetzen von Nebenwirkungen machen!

Also zurück zur Grundfrage:

Ab wann werden unveränderliche Klassen zur Last?

Sie sind immer eine Belastung für Sprachen, bei denen es mehr um Veränderbarkeit als um Unveränderlichkeit geht. Deshalb sollten Sie sie meiner Meinung nach dort einsetzen, wo der Nutzen die Kosten deutlich überwiegt. Aber auf einer Ebene, die breit genug für ausreichend große Datenstrukturen ist, gibt es meines Erachtens viele Fälle, in denen dies einen angemessenen Kompromiss darstellt.

Auch in meinem Fall habe ich nur wenige unveränderliche Datentypen und alle sind riesige Datenstrukturen, die eine große Anzahl von Elementen speichern sollen (Pixel eines Bildes / einer Textur, Entitäten und Komponenten eines ECS und Eckpunkte / Kanten / Polygone von ein Netz).


quelle