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