Nur im Allgemeinen unveränderliche Typen, die in Sprachen erstellt wurden, die sich nicht um Unveränderlichkeit drehen, kosten in der Regel mehr Entwicklerzeit für die Erstellung und potenzielle Verwendung, wenn sie einen Objekttyp vom Typ "Builder" benötigen, um die gewünschten Änderungen auszudrücken (dies bedeutet nicht, dass der Gesamtwert Arbeit wird mehr sein, aber in diesen Fällen entstehen Kosten im Voraus. Unabhängig davon, ob die Sprache das Erstellen unveränderlicher Typen wirklich einfach macht oder nicht, ist für nicht triviale Datentypen in der Regel immer ein gewisser Verarbeitungs- und Speicheraufwand erforderlich.
Funktionen ohne Nebenwirkungen machen
Wenn Sie in Sprachen arbeiten, in denen es nicht um Unveränderlichkeit geht, besteht der pragmatische Ansatz meines Erachtens nicht darin, jeden einzelnen Datentyp unveränderlich zu machen. Eine potenziell weitaus produktivere Denkweise, die Ihnen viele der gleichen Vorteile bietet, besteht darin, sich darauf zu konzentrieren, die Anzahl der Funktionen in Ihrem System zu maximieren, die keine Nebenwirkungen verursachen .
Als einfaches Beispiel, wenn Sie eine Funktion haben, die eine Nebenwirkung wie diese verursacht:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Dann brauchen wir keinen unveränderlichen ganzzahligen Datentyp, der Operatoren wie die Zuweisung nach der Initialisierung verbietet, damit diese Funktion Nebenwirkungen vermeidet. Wir können dies einfach tun:
// Returns the absolute value of 'x'.
int abs(int x);
Jetzt spielt die Funktion nicht mit x
oder außerhalb ihres Geltungsbereichs, und in diesem trivialen Fall haben wir möglicherweise sogar einige Zyklen rasiert, indem wir den mit der Indirektion / dem Aliasing verbundenen Overhead vermieden haben. Zumindest sollte die zweite Version nicht rechenintensiver sein als die erste.
Dinge, die teuer sind, um vollständig zu kopieren
Natürlich sind die meisten Fälle nicht so trivial, wenn wir vermeiden wollen, dass eine Funktion Nebenwirkungen verursacht. Ein komplexer realer Anwendungsfall könnte eher so aussehen:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
Zu diesem Zeitpunkt benötigt das Netz möglicherweise ein paar hundert Megabyte Speicher mit über hunderttausend Polygonen, noch mehr Scheitelpunkten und Kanten, mehreren Texturkarten, Morph-Zielen usw. Es wäre sehr teuer, das gesamte Netz zu kopieren, um dies zu erreichen transform
Funktion frei von Nebenwirkungen, wie so:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
Und in diesen Fällen wäre das Kopieren von etwas in seiner Gesamtheit normalerweise ein epischer Aufwand, bei dem ich es nützlich fand, Mesh
mit dem analogen "Builder" in eine persistente Datenstruktur und einen unveränderlichen Typ zu verwandeln , um modifizierte Versionen davon zu erstellen, damit es funktioniert kann einfach flache Teile kopieren und referenzieren, die nicht eindeutig sind. Es geht darum, Netzfunktionen schreiben zu können, die frei von Nebenwirkungen sind.
Persistente Datenstrukturen
Und in diesen Fällen, in denen das Kopieren von allem so unglaublich teuer ist, habe ich mir die Mühe gemacht, ein unveränderliches Produkt Mesh
zu entwerfen , das sich wirklich auszahlt, obwohl es im Voraus etwas hohe Kosten verursacht hat, weil es nicht nur die Thread-Sicherheit vereinfacht. Es vereinfacht auch die zerstörungsfreie Bearbeitung (so dass der Benutzer Netzoperationen überlagern kann, ohne seine Originalkopie zu ändern) und macht Systeme rückgängig (jetzt kann das Rückgängig-System nur eine unveränderliche Kopie des Netzes speichern, bevor Änderungen durch eine Operation vorgenommen werden, ohne Speicher zu sprengen use) und Ausnahmesicherheit (wenn jetzt eine Ausnahme in der obigen Funktion auftritt, muss die Funktion nicht alle Nebenwirkungen rückgängig machen und rückgängig machen, da sie zunächst keine verursacht hat).
Ich kann in diesen Fällen zuversichtlich sagen, dass die Zeit, die erforderlich ist, um diese umfangreichen Datenstrukturen unveränderlich zu machen, mehr Zeit als Kosten gespart hat, da ich die Wartungskosten dieser neuen Designs mit früheren verglichen habe, bei denen es um Veränderlichkeit und Funktionen ging, die Nebenwirkungen verursachen. und die früheren veränderlichen Designs kosten viel mehr Zeit und waren weitaus anfälliger für menschliches Versagen, insbesondere in Bereichen, die für Entwickler wirklich verlockend sind, während der Crunch-Zeit zu vernachlässigen, wie z. B. Ausnahmesicherheit.
Ich denke also, dass sich unveränderliche Datentypen in diesen Fällen wirklich auszahlen, aber nicht alles muss unveränderlich gemacht werden, um die meisten Funktionen in Ihrem System frei von Nebenwirkungen zu machen. Viele Dinge sind billig genug, um sie vollständig zu kopieren. Auch viele reale Anwendungen müssen hier und da einige Nebenwirkungen verursachen (zumindest wie das Speichern einer Datei), aber normalerweise gibt es weit mehr Funktionen, die keine Nebenwirkungen haben könnten.
Der Sinn einiger unveränderlicher Datentypen besteht für mich darin, sicherzustellen, dass wir die maximale Anzahl von Funktionen schreiben können, die frei von Nebenwirkungen sind, ohne epischen Overhead in Form des tiefen Kopierens massiver Datenstrukturen nach links und rechts in vollem Umfang zu verursachen, wenn nur kleine Teile vorhanden sind von ihnen müssen geändert werden. Wenn in diesen Fällen persistente Datenstrukturen vorhanden sind, wird dies zu einem Optimierungsdetail, mit dem wir unsere Funktionen so schreiben können, dass sie frei von Nebenwirkungen sind, ohne dafür epische Kosten zu zahlen.
Unveränderlicher Overhead
Konzeptionell haben die veränderlichen Versionen immer einen Effizienzvorteil. Mit unveränderlichen Datenstrukturen ist immer dieser Rechenaufwand verbunden. Aber ich fand es in den oben beschriebenen Fällen einen würdigen Austausch, und Sie können sich darauf konzentrieren, den Overhead so gering wie möglich zu halten. Ich bevorzuge diese Art von Ansatz, bei dem die Korrektheit einfach und die Optimierung schwieriger wird als die Optimierung, aber die Korrektheit schwieriger wird. Es ist bei weitem nicht so demoralisierend, Code zu haben, der perfekt funktioniert und weitere Verbesserungen an Code benötigt, der überhaupt nicht richtig funktioniert, egal wie schnell er seine falschen Ergebnisse erzielt.