Was sind die Nachteile unveränderlicher Typen?

12

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 Weathereine 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 Temperatureoder Skynicht zu ändern, und das Ändern CorrespondingCitymacht 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; } }
}
Arseni Mourzenko
quelle
3
"Es erfordert mehr Arbeit" - Zitieren erforderlich. Nach meiner Erfahrung erfordert es weniger Arbeit.
Konrad Rudolph
1
@KonradRudolph: Mit mehr Arbeit meine ich mehr Code zum Schreiben, um eine unveränderliche Klasse zu erstellen. Das Beispiel aus meiner Frage veranschaulicht dies mit 7 Zeilen für eine veränderbare Klasse und 19 für eine unveränderliche.
Arseni Mourzenko
Sie können die Code-Eingabe reduzieren, indem Sie die Funktion "Code-Snippets" in Visual Studio verwenden, wenn Sie sie verwenden. Sie können Ihre benutzerdefinierten Snippets erstellen und sich von der IDE mit wenigen Schlüsseln gleichzeitig das Feld und die Eigenschaft definieren lassen. Unveränderliche Typen sind für Multithreading unerlässlich und werden häufig in Sprachen wie Scala verwendet.
Mert Akcakaya
@Mert: Code-Schnipsel eignen sich hervorragend für einfache Dinge. Das Schreiben eines Code-Snippets, das eine vollständige Klasse mit Kommentaren zu Feldern und Eigenschaften und korrekter Reihenfolge erstellt, wäre keine leichte Aufgabe.
Arseni Mourzenko
5
Ich bin mit dem gegebenen Beispiel nicht einverstanden, die unveränderliche Version macht mehr und andere Dinge. Sie können die Variablen auf Instanzebene entfernen, indem Sie Eigenschaften mit Accessoren deklarieren {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.
Phoshi

Antworten:

16

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:

  1. Implementierungsaufwand im Vergleich zu veränderlichen Typen. Bei einem unveränderlichen Typ benötigen Sie einen Konstruktor, der Argumente für alle Eigenschaften benötigt. Ihr Beispiel ist gut. Stellen Sie sich vor, Sie haben 10 Klassen mit jeweils 5-10 Eigenschaften. Um die Dinge einfacher, müssen Sie eine Builder - Klasse haben , zu konstruieren oder erstellen modifizierten unveränderliche Instanzen in eine Weise ähnlich StringBuilderoder UriBuilderin C # oder WeatherBuilderin Ihrem Fall. Dies ist der Hauptgrund für mich, da viele Klassen, die ich entwerfe, eine solche Anstrengung nicht wert sind.
  2. Benutzerfreundlichkeit für Verbraucher . Ein unveränderlicher Typ ist im Vergleich zu einem veränderlichen Typ schwieriger zu verwenden. Für die Instanziierung müssen alle Werte initialisiert werden. Unveränderlichkeit bedeutet auch, dass wir die Instanz nicht an eine Methode übergeben können, um ihren Wert zu ändern, ohne einen Builder zu verwenden. Wenn wir einen Builder benötigen, liegt der Nachteil in my (1).
  3. Kompatibilität mit dem Sprachrahmen. Viele der Datenframeworks erfordern veränderbare Typen und Standardkonstruktoren, um zu funktionieren. Beispielsweise können Sie keine verschachtelten LINQ-zu-SQL-Abfragen mit unveränderlichen Typen durchführen und Eigenschaften, die in Editoren wie der TextBox von Windows Forms bearbeitet werden sollen, nicht binden.

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.

tia
quelle
11
"Die Instanziierung benötigt alle Werte im Voraus.": Auch ein veränderlicher Typ, es sei denn, Sie akzeptieren das Risiko, dass ein Objekt mit nicht initialisierten Feldern herumschwebt ...
Giorgio
@Giorgio Bei veränderlichem Typ sollte der Standardkonstruktor die Instanz auf den Standardstatus initialisieren, und der Status der Instanz kann später nach der Instanziierung geändert werden.
Tia
8
Für einen unveränderlichen Typ können Sie denselben Standardkonstruktor verwenden und später mit einem anderen Konstruktor eine Kopie erstellen. Wenn die Standardwerte für den veränderlichen Typ gültig sind, sollten sie auch für den unveränderlichen Typ gültig sein, da Sie in beiden Fällen dieselbe Entität modellieren. Oder was ist der Unterschied?
Giorgio
1
Eine weitere zu berücksichtigende Sache ist, was der Typ darstellt. Datenverträge sind aufgrund all dieser Punkte keine guten unveränderlichen Typen, aber Diensttypen, die mit Abhängigkeiten initialisiert werden oder nur Daten lesen und dann Operationen ausführen, sind für die Unveränderlichkeit von großer Bedeutung, da die Operationen konsistent ausgeführt werden und der Status des Dienstes nicht sein kann geändert, um das zu riskieren.
Kevin
1
Ich codiere derzeit in F #, wobei Unveränderlichkeit die Standardeinstellung ist (daher einfacher zu implementieren). Ich finde, dass Ihr Punkt 3 die große Hürde ist. Sobald Sie die meisten Standard-.NET-Bibliotheken verwenden, müssen Sie durch die Rahmen springen. (Und wenn sie Reflexion verwenden und damit Unveränderlichkeit umgehen ... argh!)
Guran
5

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 xoder 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 transformFunktion 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, Meshmit 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 Meshzu 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
3

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

mrpyo
quelle
1
Ihre beiden Probleme sind jedoch nicht in Haskell.
Florian Margaine
@FlorianMargaine Könnten Sie näher darauf eingehen?
Mrpyo
Die Langsamkeit ist dank eines intelligenten Compilers nicht wahr. Und in Haskell erfolgt sogar E / A über eine unveränderliche API.
Florian Margaine
2
Ein grundlegenderes Problem als die Geschwindigkeit besteht darin, dass es für unveränderliche Objekte schwierig ist, eine Identität aufrechtzuerhalten, während sich ihr Zustand ändert. Wenn ein veränderliches CarObjekt 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. Wenn Cares unveränderlich wäre, wäre es wahrscheinlich viel schwieriger, den aktuellen Aufenthaltsort zu finden.
Supercat
Manchmal muss der Compiler ziemlich intelligent codieren, um herauszufinden, dass kein Verweis auf das vorherige zurückgelassene Objekt vorhanden ist, und kann es daher an Ort und Stelle ändern oder Entwaldungstransformationen durchführen, et al. Besonders in größeren Programmen. Und wie @supercat sagt, kann Identität tatsächlich zu einem Problem werden.
Macke
0

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:

  • Verwenden eines veränderlichen Verweises auf ein unveränderliches Objekt
  • Verwenden eines unveränderlichen Verweises auf ein veränderliches Objekt
  • Verwenden einer veränderlichen Referenz auf ein veränderbares Objekt

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.

Superkatze
quelle
0

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 Mit

  • Es gibt eine Möglichkeit zu sagen, dass das Ergebnis eines bestimmten Methodenaufrufs (dh ImmutableList<T>.Add) nicht verworfen werden kann oder zumindest eine Warnung erzeugt

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

Kofifus
quelle
1
Zum dritten Punkt hat ReSharper ein MustUseReturnValueAttributebenutzerdefiniertes Attribut, das genau das tut. PureAttributehat den gleichen Effekt und ist dafür noch besser.
Sebastian Redl
-1

Warum werden unveränderliche Typen in anderen Anwendungen so selten verwendet?

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.

Telastyn
quelle
"Ingenieure, die nicht auf dem neuesten Stand sind": Man könnte sagen, dass ein Ingenieur auch etwas über Nicht-Mainstream-Technologien lernen sollte. Die Idee der Unveränderlichkeit wird erst seit kurzem zum Mainstream, aber sie ist eine ziemlich alte Idee und wird von älteren Sprachen wie Scheme, SML, Haskell unterstützt (wenn nicht durchgesetzt). Jeder, der es gewohnt ist, über die gängigen Sprachen hinauszuschauen, hätte es schon vor 30 Jahren erfahren können.
Giorgio
@Giorgio: In einigen Ländern schreiben viele Ingenieure immer noch C # -Code ohne LINQ, ohne FP, ohne Iteratoren und ohne Generika. Sie haben also irgendwie alles verpasst, was seit 2003 mit C # passiert ist. Wenn sie nicht einmal ihre bevorzugte Sprache kennen Ich kann mir kaum vorstellen, dass sie eine Nicht-Mainstream-Sprache beherrschen.
Arseni Mourzenko
@MainMa: Gut, dass du das Wort Ingenieure kursiv geschrieben hast .
Giorgio
@Giorgio: In meinem Land werden sie auch als Architekten , Berater und viele andere schwärmerische Begriffe bezeichnet, die nie kursiv geschrieben sind. In der Firma, in der ich derzeit arbeite, werde ich als Analystenentwickler bezeichnet , und es wird erwartet, dass ich meine Zeit damit verbringe, CSS für beschissenen alten HTML-Code zu schreiben. Berufsbezeichnungen sind auf so vielen Ebenen störend.
Arseni Mourzenko
1
@ MainMa: Ich stimme zu. Titel wie Ingenieur oder Berater sind oft nur Schlagworte ohne allgemein akzeptierte Bedeutung. Sie werden oft verwendet, um jemanden oder seine Position wichtiger / prestigeträchtiger zu machen, als es tatsächlich ist.
Giorgio