Ich sehe mich immer unveränderlicher Typen, wenn nicht erwartet wird, dass die Instanzen der Klasse geändert werden . Es erfordert mehr Arbeit (siehe Beispiel unten), erleichtert jedoch die Verwendung der Typen in einer Multithread-Umgebung.
Gleichzeitig sehe ich in anderen Anwendungen selten unveränderliche Typen, selbst wenn die Veränderlichkeit niemandem nützen würde.
Frage: Warum werden unveränderliche Typen in anderen Anwendungen so selten verwendet?
- Liegt das daran, dass das Schreiben von Code für einen unveränderlichen Typ länger dauert?
- Oder fehlt mir etwas und es gibt einige wichtige Nachteile bei der Verwendung unveränderlicher Typen?
Beispiel aus dem wirklichen Leben
Angenommen, Sie erhalten Weather
eine solche RESTful-API:
public Weather FindWeather(string city)
{
// TODO: Load the JSON response from the RESTful API and translate it into an instance
// of the Weather class.
}
Was wir im Allgemeinen sehen würden, ist (neue Zeilen und Kommentare wurden entfernt, um den Code zu verkürzen):
public sealed class Weather
{
public City CorrespondingCity { get; set; }
public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
public int PrecipitationRisk { get; set; }
public int Temperature { get; set; }
}
Auf der anderen Seite würde ich es so schreiben Weather
, da es seltsam wäre , ein von der API zu erhalten und es dann zu modifizieren: das Wetter in der realen Welt zu ändern Temperature
oder Sky
nicht zu ändern, und das Ändern CorrespondingCity
macht auch keinen Sinn.
public sealed class Weather
{
private readonly City correspondingCity;
private readonly SkyState sky;
private readonly int precipitationRisk;
private readonly int temperature;
public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
int temperature)
{
this.correspondingCity = correspondingCity;
this.sky = sky;
this.precipitationRisk = precipitationRisk;
this.temperature = temperature;
}
public City CorrespondingCity { get { return this.correspondingCity; } }
public SkyState Sky { get { return this.sky; } }
public int PrecipitationRisk { get { return this.precipitationRisk; } }
public int Temperature { get { return this.temperature; } }
}
quelle
{get; private set;}
, und selbst die veränderbare sollte einen Konstruktor haben, da alle diese Felder immer festgelegt werden sollten und warum sollten Sie das nicht erzwingen? Wenn Sie diese beiden völlig vernünftigen Änderungen vornehmen, werden die Funktionen und die LoC-Parität verbessert.Antworten:
Ich programmiere in C # und Objective-C. Ich mag unveränderliches Tippen sehr, aber im wirklichen Leben war ich aus folgenden Gründen immer gezwungen, seine Verwendung, hauptsächlich für Datentypen, einzuschränken:
StringBuilder
oderUriBuilder
in C # oderWeatherBuilder
in Ihrem Fall. Dies ist der Hauptgrund für mich, da viele Klassen, die ich entwerfe, eine solche Anstrengung nicht wert sind.Kurz gesagt, Unveränderlichkeit ist gut für Objekte, die sich wie Werte verhalten oder nur wenige Eigenschaften haben. Bevor Sie etwas unveränderlich machen, müssen Sie den erforderlichen Aufwand und die Verwendbarkeit der Klasse selbst berücksichtigen, nachdem Sie sie unveränderlich gemacht haben.
quelle
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:
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:
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:
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: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.
quelle
Der einzige Nachteil, den ich mir vorstellen kann, ist, dass die Verwendung unveränderlicher Daten theoretisch langsamer sein kann als die veränderlichen - es ist langsamer, eine neue Instanz zu erstellen und eine vorherige zu sammeln, als eine vorhandene zu ändern.
Das andere "Problem" ist, dass Sie nicht nur unveränderliche Typen verwenden können. Am Ende müssen Sie den Status beschreiben und dafür veränderbare Typen verwenden - ohne den Status zu ändern, können Sie keine Arbeit erledigen.
Die allgemeine Regel lautet jedoch, unveränderliche Typen zu verwenden, wo immer Sie können, und Typen nur dann veränderbar zu machen, wenn es wirklich einen Grund dafür gibt ...
Und um die Frage zu beantworten: " Warum werden unveränderliche Typen in anderen Anwendungen so selten verwendet? " - Ich glaube wirklich nicht, dass sie ... wo immer Sie hinschauen, jeder empfiehlt, Ihre Klassen so unveränderlich wie möglich zu gestalten ... zum Beispiel: http://www.javapractices.com/topic/TopicAction.do?Id=29
quelle
Car
Objekt kontinuierlich mit dem Standort eines bestimmten physischen Automobils aktualisiert wird, kann ich, wenn ich einen Verweis auf dieses Objekt habe, schnell und einfach den Aufenthaltsort dieses Automobils herausfinden. WennCar
es unveränderlich wäre, wäre es wahrscheinlich viel schwieriger, den aktuellen Aufenthaltsort zu finden.Um ein reales System zu modellieren, in dem sich Dinge ändern können, muss der veränderbare Zustand irgendwie irgendwo codiert werden. Es gibt drei Möglichkeiten, wie ein Objekt einen veränderlichen Zustand annehmen kann:
Die Verwendung von first erleichtert es einem Objekt, einen unveränderlichen Schnappschuss des aktuellen Zustands zu erstellen. Die Verwendung der zweiten Option erleichtert es einem Objekt, eine Live-Ansicht des aktuellen Status zu erstellen. Die Verwendung der dritten Option kann manchmal bestimmte Aktionen effizienter machen, wenn kaum ein unveränderlicher Bedarf an unveränderlichen Schnappschüssen oder Live-Ansichten zu erwarten ist.
Abgesehen von der Tatsache, dass die Aktualisierung des Zustands, der unter Verwendung einer veränderlichen Referenz auf ein unveränderliches Objekt gespeichert wird, häufig langsamer ist als die Aktualisierung des Zustands, der unter Verwendung eines veränderlichen Objekts gespeichert ist, muss bei Verwendung einer veränderlichen Referenz auf die Möglichkeit verzichtet werden, eine billige Live-Ansicht des Zustands zu erstellen. Wenn Sie keine Live-Ansicht erstellen müssen, ist dies kein Problem. Wenn jedoch eine Live-Ansicht erstellt werden muss, führt die Unfähigkeit, eine unveränderliche Referenz zu verwenden, zu allen Operationen mit der Ansicht - sowohl zum Lesen als auch zum Schreiben- viel langsamer als sonst. Wenn der Bedarf an unveränderlichen Snapshots den Bedarf an Live-Ansichten übersteigt, kann die verbesserte Leistung von unveränderlichen Snapshots den Leistungseinbruch für Live-Ansichten rechtfertigen. Wenn Sie jedoch Live-Ansichten benötigen und keine Snapshots benötigen, sollten Sie unveränderliche Verweise auf veränderbare Objekte verwenden gehen.
quelle
In Ihrem Fall liegt die Antwort hauptsächlich darin, dass C # die Unveränderlichkeit nur unzureichend unterstützt ...
Es wäre toll, wenn:
Sofern nicht anders angegeben (dh mit einem veränderlichen Schlüsselwort), ist standardmäßig alles unveränderlich. Das Mischen von unveränderlichen und veränderlichen Typen ist verwirrend
Mutationsmethoden (
With
) werden automatisch verfügbar sein - dies kann jedoch bereits erreicht werden, siehe MitEs gibt eine Möglichkeit zu sagen, dass das Ergebnis eines bestimmten Methodenaufrufs (dh
ImmutableList<T>.Add
) nicht verworfen werden kann oder zumindest eine Warnung erzeugtUnd vor allem, wenn der Compiler auf Anfrage so weit wie möglich die Unveränderlichkeit sicherstellen kann (siehe https://github.com/dotnet/roslyn/issues/159 ).
quelle
MustUseReturnValueAttribute
benutzerdefiniertes Attribut, das genau das tut.PureAttribute
hat den gleichen Effekt und ist dafür noch besser.Ignoranz? Unerfahrenheit?
Unveränderliche Objekte werden heutzutage allgemein als überlegen angesehen, aber es ist eine relativ junge Entwicklung. Ingenieure, die nicht auf dem neuesten Stand sind oder einfach nur in dem stecken, was sie wissen, werden sie nicht verwenden. Und es sind einige Designänderungen erforderlich, um sie effektiv zu nutzen. Wenn die Apps alt sind oder die Ingenieure nicht über ausreichende Designkenntnisse verfügen, kann die Verwendung dieser Apps umständlich oder auf andere Weise problematisch sein.
quelle