Perché le strutture mutevoli sono “cattive”?

Seguendo le discussioni qui su SO ho già letto più volte l’osservazione che le strutture mutabili sono “cattive” (come nella risposta a questa domanda ).

Qual è il vero problema con la mutabilità e le strutture in C #?

Le strutture sono tipi di valore che significa che vengono copiati quando vengono passati in giro.

Quindi se cambi una copia stai cambiando solo quella copia, non l’originale e non altre copie che potrebbero essere in giro.

Se la tua struct è immutabile, tutte le copie automatiche risultanti dall’essere passati per valore saranno uguali.

Se vuoi cambiarlo devi farlo coscientemente creando una nuova istanza della struct con i dati modificati. (non una copia)

Da dove cominciare ;-p

Il blog di Eric Lippert è sempre buono per una citazione:

Questo è ancora un altro motivo per cui i tipi di valore mutabili sono malvagi. Cerca di rendere sempre immutabili i tipi di valore.

In primo luogo, tendi a perdere le modifiche abbastanza facilmente … ad esempio, tirando fuori le cose da una lista:

 Foo foo = list[0]; foo.Name = "abc"; 

cosa è cambiato? Niente di utile …

Lo stesso con le proprietà:

 myObj.SomeProperty.Size = 22; // the compiler spots this one 

costringendoti a fare:

 Bar bar = myObj.SomeProperty; bar.Size = 22; myObj.SomeProperty = bar; 

meno criticamente, c’è un problema di dimensioni; gli oggetti mutabili tendono ad avere proprietà multiple; tuttavia se hai una struttura con due int , una string , un DateTime e un bool , puoi bruciare molto velocemente molta memoria. Con una class, più chiamanti possono condividere un riferimento alla stessa istanza (i riferimenti sono piccoli).

Non direi male, ma la mutevolezza è spesso un segno di eccessiva attenzione da parte del programmatore per fornire il massimo della funzionalità. In realtà, questo spesso non è necessario e, a sua volta, rende l’interfaccia più piccola, più facile da usare e più difficile da usare sbagliata (= più robusta).

Un esempio di questo è conflitto di lettura / scrittura e scrittura / scrittura in condizioni di gara. Questi semplicemente non possono verificarsi in strutture immutabili, dal momento che una scrittura non è un’operazione valida.

Inoltre, sostengo che la mutabilità non è quasi mai realmente necessaria , il programmatore pensa che potrebbe essere in futuro. Ad esempio, semplicemente non ha senso cambiare una data. Piuttosto, crea una nuova data basata su quella precedente. Questa è un’operazione a basso costo, quindi le prestazioni non sono una considerazione.

Le strutture mutevoli non sono malvagie.

Sono assolutamente necessari in circostanze ad alte prestazioni. Ad esempio quando le linee della cache e / o la garbage collection diventano un collo di bottiglia.

Non chiamerei l’uso di una struttura immutabile in questi casi d’uso perfettamente “malvagi”.

Riesco a vedere il punto in cui la syntax di C # non aiuta a distinguere l’accesso di un membro di un tipo di valore o di un tipo di riferimento, quindi preferisco tutte le strutture immutabili, che rafforzano l’immutabilità, su strutture mutabili.

Tuttavia, invece di etichettare semplicemente le strutture immutabili come “malvagie”, consiglierei di abbracciare il linguaggio e sostenere una regola empirica più utile e costruttiva.

Ad esempio: “le strutture sono tipi di valore, che vengono copiati per impostazione predefinita, è necessario un riferimento se non si desidera copiarli” o “provare prima a lavorare con le strutture readonly” .

Le strutture con campi o proprietà pubbliche mutabili non sono malvagie.

I metodi di Struct (distinti dai setter di proprietà) che mutano “questo” sono in qualche modo malvagi, solo perché .net non fornisce un mezzo per distinguerli da metodi che non lo fanno. Struct metodi che non mutano “questo” dovrebbe essere invocato anche su strutture di sola lettura senza bisogno di copia difensiva. I metodi che mutano “questo” non dovrebbero essere invocabili su strutture di sola lettura. Dato che .net non vuole proibire i metodi struct che non modificano “this” da invocare su strutture di sola lettura, ma non vuole permettere che le strutture di sola lettura siano mutate, copia difensivamente le strutture in read- solo contesti, probabilmente ottenendo il peggio di entrambi i mondi.

Nonostante i problemi con la gestione di metodi auto-mutanti in contesti di sola lettura, tuttavia, le strutture mutevoli offrono spesso una semantica di gran lunga superiore ai tipi di class mutabili. Considera le seguenti tre firme di metodo:

 struct PointyStruct {public int x, y, z;};
 class PointyClass {public int x, y, z;};

 void Method1 (PointyStruct foo);
 void Method2 (ref PointyStruct foo);
 void Method3 (PointyClass pippo);

Per ogni metodo, rispondi alle seguenti domande:

  1. Supponendo che il metodo non usi alcun codice “non sicuro”, potrebbe modificare foo?
  2. Se non esistono riferimenti esterni a “pippo” prima che il metodo venga chiamato, potrebbe esistere un riferimento esterno dopo?

risposte:

Domanda 1:
Method1() : no (intento chiaro)
Method2() : sì (intento chiaro)
Method3() : sì (intento incerto)
Domanda 2:
Method1() : no
Method2() : no (se non pericoloso)
Method3() : sì

Method1 non può modificare foo e non ottiene mai un riferimento. Method2 ottiene un riferimento a foo di breve durata, che può utilizzare modificare i campi di foo qualsiasi numero di volte, in qualsiasi ordine, fino a quando non ritorna, ma non può persistere tale riferimento. Prima che il Metodo2 ritorni, a meno che non usi un codice non sicuro, tutte le copie che potrebbero essere state fatte del suo riferimento “foo” saranno scomparse. Method3, a differenza di Method2, ottiene un riferimento promiscuo e condivisibile a foo, e non si sa cosa potrebbe farci. Potrebbe non cambiare affatto, potrebbe cambiare foo e poi tornare, o potrebbe dare un riferimento a foo a un altro thread che potrebbe mutarlo in modo arbitrario in un tempo arbitrario futuro. L’unico modo per limitare ciò che Method3 potrebbe fare a un object class mutabile passato sarebbe quello di incapsulare l’object mutabile in un wrapper di sola lettura, che è brutto e ingombrante.

Le matrici di strutture offrono una semantica meravigliosa. Dato RectArray [500] di tipo Rectangle, è chiaro e ovvio come ad esempio copiare l’elemento 123 nell’elemento 456 e quindi un po ‘di tempo dopo impostare la larghezza dell’elemento 123 a 555, senza l’elemento di disturbo 456. “RectArray [432] = RectArray [321 ]; …; RectArray [123] .Width = 555; “. Sapendo che Rectangle è una struttura con un campo intero chiamato Width, diremo a tutti uno che deve sapere delle affermazioni di cui sopra.

Ora supponiamo che RectClass sia una class con gli stessi campi di Rectangle e che si desideri eseguire le stesse operazioni su un RectClassArray [500] di tipo RectClass. Forse la matrice dovrebbe contenere 500 riferimenti immutabili preinizializzati a oggetti RectClass mutabili. in tal caso, il codice corretto sarebbe qualcosa come “RectClassArray [321] .SetBounds (RectClassArray [456]); …; RectClassArray [321] .X = 555;”. Si presume che l’array contenga istanze che non cambieranno, quindi il codice corretto sarebbe più simile a “RectClassArray [321] = RectClassArray [456]; …; RectClassArray [321] = New RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; ” Per sapere cosa si dovrebbe fare, si dovrebbe sapere molto di più su RectClass (ad esempio supporta un costruttore di copie, un metodo copy-from, ecc.) E l’uso previsto dell’array. In nessun posto vicino pulito come usare una struttura.

A dire il vero, purtroppo non esiste un modo carino per qualsiasi class contenitore diversa da un array per offrire la semantica pulita di un array struct. Il meglio che si potrebbe fare, se si volesse indicizzare una collezione, ad esempio una stringa, sarebbe probabilmente quello di offrire un metodo generico “ActOnItem” che accetterebbe una stringa per l’indice, un parametro generico e un delegato che sarebbe passato facendo riferimento sia al parametro generico che alla voce di riscossione. Ciò consentirebbe quasi la stessa semantica di struct matrici, ma a meno che le persone vb.net e C # non possano essere persuase per offrire una bella syntax, il codice sarà di aspetto goffo anche se è ragionevolmente performante (passare un parametro generico sarebbe consentire l’uso di un delegato statico ed evitare qualsiasi necessità di creare istanze di class temporanee).

Personalmente, sono irritato dall’odio Eric Lippert et al. spew riguardo ai tipi di valore mutabili. Offrono una semantica più pulita rispetto ai tipi di riferimento promiscui utilizzati in tutto il luogo. Nonostante alcune delle limitazioni con il supporto di .net per i tipi di valore, ci sono molti casi in cui i tipi di valore mutabili sono più adatti di qualsiasi altro tipo di entity framework.

I tipi di valore rappresentano fondamentalmente concetti immutabili. Fx, non ha senso avere un valore matematico come un numero intero, un vettore ecc. E quindi essere in grado di modificarlo. Sarebbe come ridefinire il significato di un valore. Invece di modificare un tipo di valore, ha più senso assegnare un altro valore univoco. Pensa al fatto che i tipi di valore vengono confrontati confrontando tutti i valori delle sue proprietà. Il punto è che se le proprietà sono uguali, allora è la stessa rappresentazione universale di quel valore.

Come menziona Konrad, non ha senso cambiare anche una data, poiché il valore rappresenta quel punto univoco nel tempo e non un’istanza di un object temporale che ha uno stato o una dipendenza dal contesto.

Spera che questo abbia un senso per te. È più sul concetto che si tenta di catturare con tipi di valore che con dettagli pratici, per essere sicuri.

Ci sono altri casi di coppia che potrebbero portare a comportamenti imprevedibili dal punto di vista dei programmatori. Ecco un paio di loro.

  1. Tipi di valore immutabili e campi di sola lettura
 // Simple mutable structure. // Method IncrementI mutates current state. struct Mutable { public Mutable(int i) : this() { I = i; } public void IncrementI() { I++; } public int I {get; private set;} } // Simple class that contains Mutable structure // as readonly field class SomeClass { public readonly Mutable mutable = new Mutable(5); } // Simple class that contains Mutable structure // as ordinary (non-readonly) field class AnotherClass { public Mutable mutable = new Mutable(5); } class Program { void Main() { // Case 1. Mutable readonly field var someClass = new SomeClass(); someClass.mutable.IncrementI(); // still 5, not 6, because SomeClass.mutable field is readonly // and compiler creates temporary copy every time when you trying to // access this field Console.WriteLine(someClass.mutable.I); // Case 2. Mutable ordinary field var anotherClass = new AnotherClass(); anotherClass.mutable.IncrementI(); //Prints 6, because AnotherClass.mutable field is not readonly Console.WriteLine(anotherClass.mutable.I); } } 

  1. Tipi di valore e matrice mutabili

Supponiamo di avere una matrice della nostra struttura Mutevole e stiamo chiamando il metodo IncrementI per il primo elemento di quell’array. Quale comportamento ti aspetti da questa chiamata? Dovrebbe cambiare il valore dell’array o solo una copia?

 Mutable[] arrayOfMutables = new Mutable[1]; arrayOfMutables[0] = new Mutable(5); // Now we actually accessing reference to the first element // without making any additional copy arrayOfMutables[0].IncrementI(); //Prints 6!! Console.WriteLine(arrayOfMutables[0].I); // Every array implements IList interface IList listOfMutables = arrayOfMutables; // But accessing values through this interface lead // to different behavior: IList indexer returns a copy // instead of an managed reference listOfMutables[0].IncrementI(); // Should change I to 7 // Nope! we still have 6, because previous line of code // mutate a copy instead of a list value Console.WriteLine(listOfMutables[0].I); 

Quindi, le strutture mutevoli non sono malvagie finché tu e il resto del team capisci chiaramente cosa stai facendo. Ma ci sono troppi casi angolari in cui il comportamento del programma sarebbe diverso da quello previsto, che potrebbe portare a errori difficili da produrre e difficili da capire.

Se hai mai programmato in un linguaggio come C / C ++, le strutture vanno bene da usare come mutabili. Basta passarli con ref, in giro e non c’è nulla che possa andare storto. L’unico problema che trovo sono le restrizioni del compilatore C # e che, in alcuni casi, non riesco a forzare la cosa stupida a usare un riferimento alla struct, invece di una copia (come quando una struct fa parte di una class C # ).

Quindi, le strutture mutevoli non sono malvagie, C # le ha rese malvagie. Uso sempre strutture mutevoli in C ++ e sono molto comode e intuitive. Al contrario, C # mi ha fatto abbandonare completamente le strutture come membri delle classi a causa del modo in cui gestiscono gli oggetti. La loro convenienza ci è costata la nostra.

Immagina di avere una serie di 1.000.000 di strutture. Ogni struct rappresenta un’equity con cose come bid_price, offer_price (forse decimali) e così via, questo è creato da C # / VB.

Immaginate che l’array sia creato in un blocco di memoria allocato nell’heap non gestito in modo che qualche altro thread di codice nativo sia in grado di accedere simultaneamente all’array (forse un codice ad alta definizione che fa matematica).

Immaginate che il codice C # / VB stia ascoltando un feed di mercato di variazioni di prezzo, che il codice possa dover accedere ad alcuni elementi dell’array (per qualsiasi sicurezza) e quindi modificare alcuni campi di prezzo.

Immagina che questo venga fatto decine o addirittura centinaia di migliaia di volte al secondo.

Bene, lascia fare i fatti, in questo caso vogliamo davvero che queste strutture siano mutabili, devono essere perché sono condivise da qualche altro codice nativo, quindi creare copie non aiuterà; devono essere perché fare una copia di una struttura di 120 byte a queste frequenze è una follia, specialmente quando un aggiornamento può effettivamente avere un impatto solo di un byte o due.

Hugo

Se ci si attiene a quali strutture sono intese (in C #, Visual Basic 6, Pascal / Delphi, tipo struct C ++ (o classi) quando non vengono utilizzate come puntatori), si scoprirà che una struttura non è più di una variabile composta . Ciò significa: li tratterai come un set compresso di variabili, sotto un nome comune (una variabile di record a cui fai riferimento come membri).

So che confonderebbe un sacco di persone profondamente abituate a OOP, ma non è una ragione sufficiente per dire che tali cose sono intrinsecamente malvagie, se usate correttamente. Alcune strutture sono immutabili come intendono (questo è il caso della namedtuple di Python), ma è un altro paradigma da considerare.

Sì: le strutture coinvolgono molta memoria, ma non sarà precisamente più memoria facendo:

 point.x = point.x + 1 

rispetto a:

 point = Point(point.x + 1, point.y) 

Il consumo di memoria sarà almeno lo stesso, o anche più nel caso immutabile (anche se quel caso sarebbe temporaneo, per lo stack corrente, a seconda della lingua).

Ma, infine, le strutture sono strutture , non oggetti. In POO, la proprietà principale di un object è la loro identity framework , che la maggior parte delle volte non è superiore al suo indirizzo di memoria. Struct indica la struttura dei dati (non un object appropriato, quindi non hanno id quadro) e i dati possono essere modificati. In altre lingue, il record (invece di struct , come nel caso di Pascal) è la parola e ha lo stesso scopo: solo una variabile del record di dati, intesa per essere letta da file, modificata e riversata in file (che è il principale usa e, in molte lingue, puoi anche definire l’allineamento dei dati nel record, mentre non è necessariamente il caso degli oggetti correttamente chiamati).

Vuoi un buon esempio? Le strutture sono usate per leggere facilmente i file. Python ha questa libreria perché, essendo orientata agli oggetti e senza supporto per le strutture, ha dovuto implementarla in un altro modo, che è alquanto brutto. Le lingue che implementano le strutture hanno questa caratteristica … built-in. Try reading a bitmap header with an appropriate struct in languages like Pascal or C. It will be easy (if the struct is properly built and aligned; in Pascal you would not use a record-based access but functions to read arbitrary binary data). So, for files and direct (local) memory access, structs are better than objects. As for today, we’re used to JSON and XML, and so we forget the use of binary files (and as a side effect, the use of structs). But yes: they exist, and have a purpose.

They are not evil. Just use them for the right purpose.

If you think in terms of hammers, you will want to treat screws as nails, to find screws are harder to plunge in the wall, and it will be screws’ fault, and they will be the evil ones.

When something can be mutated, it gains a sense of identity.

 struct Person { public string name; // mutable public Point position = new Point(0, 0); // mutable public Person(string name, Point position) { ... } } Person eric = new Person("Eric Lippert", new Point(4, 2)); 

Because Person is mutable, it’s more natural to think about changing Eric’s position than cloning Eric, moving the clone, and destroying the original . Both operations would succeed in changing the contents of eric.position , but one is more intuitive than the other. Likewise, it’s more intuitive to pass Eric around (as a reference) for methods to modify him. Giving a method a clone of Eric is almost always going to be surprising. Anyone wanting to mutate Person must remember to ask for a reference to Person or they’ll be doing the wrong thing.

If you make the type immutable, the problem goes away; if I can’t modify eric , it makes no difference to me whether I receive eric or a clone of eric . More generally, a type is safe to pass by value if all of its observable state is held in members that are either:

  • immutable
  • reference types
  • safe to pass by value

If those conditions are met then a mutable value type behaves like a reference type because a shallow copy will still allow the receiver to modify the original data.

The intuitiveness of an immutable Person depends on what you’re trying to do though. If Person just represents a set of data about a person, there’s nothing unintuitive about it; Person variables truly represent abstract values , not objects. (In that case, it’d probably be more appropriate to rename it to PersonData .) If Person is actually modeling a person itself, the idea of constantly creating and moving clones is silly even if you’ve avoided the pitfall of thinking you’re modifying the original. In that case it’d probably be more natural to simply make Person a reference type (that is, a class.)

Granted, as functional programming has taught us there are benefits to making everything immutable (no one can secretly hold on to a reference to eric and mutate him), but since that’s not idiomatic in OOP it’s still going to be unintuitive to anyone else working with your code.

It doesn’t have anything to do with structs (and not with C#, either) but in Java you might get problems with mutable objects when they are eg keys in a hash map. If you change them after adding them to a map and it changes its hash code , evil things might happen.

Personally when I look at code the following looks pretty clunky to me:

data.value.set ( data.value.get () + 1 ) ;

rather than simply

data.value++ ; or data.value = data.value + 1 ;

Data encapsulation is useful when passing a class around and you want to ensure the value is modified in a controlled fashion. However when you have public set and get functions that do little more than set the value to what ever is passed in, how is this an improvement over simply passing a public data structure around?

When I create a private structure inside a class, I created that structure to organize a set of variables into one group. I want to be able to modify that structure within the class scope, not get copies of that structure and create new instances.

To me this prevents a valid use of structures being used to organize public variables, if I wanted access control I’d use a class.

There are many advantages and disadvantages to mutable data. The million-dollar disadvantage is aliasing. If the same value is being used in multiple places, and one of them changes it, then it will appear to have magically changed to the other places that are using it. This is related to, but not identical with, race conditions.

The million-dollar advantage is modularity, sometimes. Mutable state can allow you to hide changing information from code that doesn’t need to know about it.

The Art of the Interpreter goes into these trade offs in some detail, and gives some examples.

There are several issues with Mr. Eric Lippert’s example. It is contrived to illustrate the point that structs are copied and how that could be a problem if you are not careful. Looking at the example I see it as a result of a bad programming habit and not really a problem with either struct or the class.

  1. A struct is supposed to have only public members and should not require any encapsulation. If it does then it really should be a type/class. You really do not need two constructs to say the same thing.

  2. If you have class enclosing a struct, you would call a method in the class to mutate the member struct. This is what I would do as a good programming habit.

A proper implementation would be as follows.

 struct Mutable { public int x; } class Test { private Mutable m = new Mutable(); public int mutate() { mx = mx + 1; return mx; } } static void Main(string[] args) { Test t = new Test(); System.Console.WriteLine(t.mutate()); System.Console.WriteLine(t.mutate()); System.Console.WriteLine(t.mutate()); } 

It looks like it is an issue with programming habit as opposed to an issue with struct itself. Structs are supposed to be mutable, that is the idea and intent.

The result of the changes voila behaves as expected:

1 2 3 Press any key to continue . . .

I don’t believe they’re evil if used correctly. I wouldn’t put it in my production code, but I would for something like structured unit testing mocks, where the lifespan of a struct is relatively small.

Using the Eric example, perhaps you want to create a second instance of that Eric, but make adjustments, as that’s the nature of your test (ie duplication, then modifying). It doesn’t matter what happens with the first instance of Eric if we’re just using Eric2 for the remainder of the test script, unless you’re planning on using him as a test comparison.

This would be mostly useful for testing or modifying legacy code that shallow defines a particular object (the point of structs), but by having an immutable struct, this prevents it’s usage annoyingly.