Perché le strutture mutevoli sono "malvagie"?


485

Dopo le discussioni qui su SO ho già letto più volte l'osservazione che le strutture mutabili sono "malvagie" (come nella risposta a questa domanda ).

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


12
Affermare che le strutture mutabili sono malvagie è come rivendicare mutabili ints, se booltutti gli altri tipi di valore sono cattivi. Esistono casi di mutabilità e immutabilità. Questi casi dipendono dal ruolo svolto dai dati, non dal tipo di allocazione / condivisione della memoria.
Slipp D. Thompson,

41
@slipp inte nonbool sono mutabili ..
Blorgbeard esce il

2
... .-sintassi, rendendo le operazioni con dati ref-digitati e dati digitati sul valore sembrano uguali anche se sono nettamente diverse. Questo è un errore delle proprietà di C #, non delle strutture: alcune lingue offrono un'alternativaa[V][X] = 3.14 sintassi per la mutazione sul posto. In C #, faresti meglio a offrire metodi di mutatore di membri della struttura come 'MutateV (Azione <ref Vector2> mutatore) `e usarlo come a.MutateV((v) => { v.X = 3; }) (esempio è troppo semplificato a causa delle limitazioni che C # ha riguardo alla refparola chiave, ma con alcuni soluzioni alternative dovrebbero essere possibili) .
Slipp D. Thompson,

2
@Slipp Beh, penso esattamente il contrario di questo tipo di strutture. Perché pensi che le strutture già implementate nella libreria .NET, come DateTime o TimeSpan (così simili) siano immutabili? Forse potrebbe essere utile cambiare un solo membro della var di tale struttura, ma è troppo scomodo e porta a troppi problemi. In realtà ti sbagli su ciò che calcola il processore, poiché C # non si compila in assembler, si compila in IL. In IL (a condizione che abbiamo già la variabile denominata x) questa singola operazione è di 4 istruzioni: ldloc.0(carica la variabile 0-index in ...
Sushi271

1
... genere. Tè il tipo. Ref è solo una parola chiave che fa passare la variabile a un metodo stesso, non una sua copia. Ha anche senso per i tipi di riferimento, poiché possiamo cambiare la variabile , ovvero il riferimento esterno al metodo punterà ad altro oggetto dopo essere stato modificato all'interno del metodo. Poiché ref Tnon è un tipo, ma la moda di passare un parametro di metodo, non è possibile inserirlo <>, perché solo i tipi possono essere inseriti lì. Quindi è semplicemente errato. Forse sarebbe conveniente farlo, forse il team C # potrebbe farlo per qualche nuova versione, ma in questo momento stanno lavorando ad alcuni ...
Sushi271

Risposte:


290

Le strutture sono tipi di valore, il che significa che vengono copiate quando vengono passate in giro.

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

Se la tua struttura è immutabile, tutte le copie automatiche risultanti dal passaggio per valore saranno le stesse.

Se si desidera modificarlo, è necessario farlo consapevolmente creando una nuova istanza della struttura con i dati modificati. (non una copia)


82
"Se la tua struttura è immutabile, tutte le copie saranno uguali." No, significa che devi fare consapevolmente una copia se vuoi un valore diverso. Significa che non verrai sorpreso a modificare una copia pensando di modificare l'originale.
Lucas,

19
@Lucas Penso che tu stia parlando di un diverso tipo di copia Sto parlando delle copie automatiche fatte a seguito del loro passaggio per valore, La tua 'copia coscientemente fatta' è diversa di proposito non l'hai fatta per errore e il suo non proprio una copia è un nuovo istante deliberato contenente dati diversi.
trampster,

3
La tua modifica (16 mesi dopo) lo rende un po 'più chiaro. Rimango ancora in piedi "(immutabile struct) significa che non verrai sorpreso a modificare una copia pensando che stai modificando l'originale", però.
Lucas,

6
@Lucas: Il pericolo di fare una copia di una struttura, modificarla e in qualche modo pensare di modificare l'originale (quando il fatto che si stia scrivendo un campo di struttura rende evidente il fatto che si sta solo scrivendo la propria copia) piuttosto piccolo rispetto al pericolo che qualcuno che detenga un oggetto di classe come mezzo per conservare le informazioni in esso contenute mutazione dell'oggetto per aggiornare le proprie informazioni e nel processo corrompere le informazioni detenute da qualche altro oggetto.
supercat,

2
Il terzo paragrafo sembra nella migliore delle ipotesi sbagliato o poco chiaro. Se la tua struttura è immutabile, semplicemente non sarai in grado di modificare i suoi campi o i campi delle copie fatte. "Se si vuole cambiare si deve ..." che è fuorviante troppo, non si può cambiare che mai , nè consciamente nè inconsciamente. La creazione di una nuova istanza in cui i dati desiderati non ha nulla a che fare con la copia originale se non la stessa struttura di dati.
Saeb Amini,

167

Da dove iniziare ;-p

Il blog di Eric Lippert è sempre buono per un preventivo:

Questa è un'altra ragione per cui i tipi di valori mutabili sono malvagi. Cerca di rendere sempre immutabili i tipi di valore.

Innanzitutto, tendi a perdere le modifiche abbastanza facilmente ... ad esempio, togliendo le cose da un elenco:

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

che 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 più proprietà; ma se hai uno struct con due ints, a string, a DateTimee a bool, puoi bruciare molto rapidamente molta memoria. Con una classe, più chiamanti possono condividere un riferimento alla stessa istanza (i riferimenti sono piccoli).


5
Beh sì, ma il compilatore è semplicemente stupido in quel modo. Non consentire l'assegnazione ai membri della struttura immobiliare è stata IMHO una stupida decisione di progettazione, poiché è consentita per l' ++operatore. In questo caso, il compilatore scrive semplicemente l'assegnazione esplicita stessa invece di affrettare il programmatore.
Konrad Rudolph,

12
@Konrad: myObj.SomeProperty.Size = 22 modifica una COPIA di myObj.SomeProperty. Il compilatore ti sta salvando da un evidente bug. E NON è consentito per ++.
Lucas,

@Lucas: Beh, il compilatore Mono C # lo consente certamente - dal momento che non ho Windows non riesco a controllare il compilatore di Microsoft.
Konrad Rudolph,

6
@Konrad - con una indiretta in meno dovrebbe funzionare; è il "mutare un valore di qualcosa che esiste solo come valore transitorio nello stack e che sta per evaporare nel nulla" che è il caso bloccato.
Marc Gravell

2
@Marc Gravell: Nel primo pezzo di codice, si finisce con un "Foo" il cui nome è "abc" e i cui altri attributi sono quelli di List [0], senza disturbare List [0]. Se Foo fosse una classe, sarebbe necessario clonarlo e quindi cambiare la copia. A mio avviso, il grosso problema con la distinzione valore-classe vs classe è l'uso del "." operatore per due scopi. Se avessi i miei druther, le classi potrebbero supportare entrambi "." e "->" per metodi e proprietà, ma la semantica normale per "." le proprietà sarebbero di creare una nuova istanza con il campo appropriato modificato.
supercat

71

Non direi il male, ma la mutabilità è spesso un segno di reazione eccessiva da parte del programmatore per fornire il massimo della funzionalità. In realtà, questo spesso non è necessario e ciò, a sua volta, rende l'interfaccia più piccola, più facile da usare e più difficile da usare in modo errato (= più robusto).

Un esempio è rappresentato dai conflitti di lettura / scrittura e scrittura / scrittura nelle condizioni di gara. Questi semplicemente non possono verificarsi in strutture immutabili, poiché una scrittura non è un'operazione valida.

Inoltre, sostengo che la mutabilità non è quasi mai realmente necessaria , il programmatore pensa solo che potrebbe essere in futuro. Ad esempio, semplicemente non ha senso cambiare una data. Piuttosto, crea una nuova data in base a quella precedente. Questa è un'operazione economica, quindi le prestazioni non sono da tenere in considerazione.


1
Eric Lippert dice che sono ... vedi la mia risposta.
Marc Gravell

42
Per quanto rispetto Eric Lippert, non è Dio (o almeno non ancora). Il post sul blog a cui ti colleghi e il tuo post sopra sono argomenti ragionevoli per rendere immutabili le strutture come ovvio, ma in realtà sono molto deboli come argomenti per non usare mai strutture mutabili. Questo post, tuttavia, è un +1.
Stephen Martin,

2
Sviluppando in C #, di solito hai bisogno della mutabilità ogni tanto, specialmente con il tuo modello di business, dove vuoi che lo streaming ecc. Funzioni senza problemi con le soluzioni esistenti. Ho scritto un articolo su come lavorare con dati mutabili E immutabili, risolvendo la maggior parte dei problemi relativi alla mutabilità (spero): rickyhelgesson.wordpress.com/2012/07/17/…
Ricky Helgesson

3
@StephenMartin: Le strutture che incapsulano un singolo valore spesso dovrebbero essere immutabili, ma le strutture sono di gran lunga il mezzo migliore per incapsulare insiemi fissi di variabili indipendenti ma correlate (come le coordinate X e Y di un punto) che non hanno "identità" come gruppo. Le strutture utilizzate a tale scopo dovrebbero generalmente esporre le loro variabili come campi pubblici. Considererei l'idea che sia più appropriato usare una classe piuttosto che una struttura per tali scopi è semplicemente sbagliato. Le classi immutabili sono spesso meno efficienti e le classi mutabili spesso hanno una semantica terribile.
supercat

2
@StephenMartin: si consideri, ad esempio, un metodo o una proprietà che dovrebbe restituire i sei floatcomponenti di una trasformazione grafica. Se un tale metodo restituisce una struttura a campo esposto con sei componenti, è ovvio che la modifica dei campi della struttura non modificherà l'oggetto grafico da cui è stato ricevuto. Se un tale metodo restituisce un oggetto di classe mutabile, forse cambiando le sue proprietà cambierà l'oggetto grafico sottostante e forse non lo farà - nessuno lo sa davvero.
supercat

48

Le strutture mutevoli non sono cattive.

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

Non definirei "malvagio" l'uso di una struttura immutabile in questi casi d'uso perfettamente validi.

Vedo il punto che la sintassi di C # non aiuta a distinguere l'accesso di un membro di un tipo di valore o di un tipo di riferimento, quindi sono tutto per preferire le strutture immutabili, che impongono l'immutabilità, piuttosto che le strutture mutabili.

Tuttavia, invece di etichettare semplicemente le strutture immutabili come "malvagie", consiglierei di abbracciare il linguaggio e sostenere una regola dei pollici 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 di sola lettura" .


8
Suppongo anche che se si desidera fissare un set fisso di variabili insieme al nastro adesivo in modo che i loro valori possano essere elaborati o memorizzati separatamente o come unità, ha molto più senso chiedere al compilatore di fissare un set fisso di variabili insieme (cioè dichiarare a structcon campi pubblici) piuttosto che definire una classe che può essere usata, goffamente, per raggiungere gli stessi fini, o per aggiungere un mucchio di spazzatura a una struttura per farla emulare tale classe (piuttosto che averla comportarsi come un insieme di variabili attaccate insieme al nastro adesivo, che è quello che si vuole veramente in primo luogo)
supercat

41

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

I metodi 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. I metodi Struct che non mutano "questo" dovrebbero essere invocabili anche su strutture di sola lettura senza necessità di copia difensiva. I metodi che mutano "questo" non dovrebbero essere affatto invocabili su strutture di sola lettura. Poiché .net non vuole vietare i metodi di struttura che non modificano "this" dall'essere invocati su strutture di sola lettura, ma non vuole permettere che le strutture di sola lettura vengano mutate, copia difensivamente le strutture in lettura- solo contesti, probabilmente ottenendo il peggio di entrambi i mondi.

Nonostante i problemi con la gestione dei metodi auto-mutanti in contesti di sola lettura, tuttavia, le strutture mutabili offrono spesso una semantica di gran lunga superiore ai tipi di classe 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 foo);

Per ciascun metodo, rispondi alle seguenti domande:

  1. Supponendo che il metodo non utilizzi alcun codice "non sicuro", potrebbe modificare foo?
  2. Se non esistono riferimenti esterni a "pippo" prima che venga chiamato il metodo, 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 (a meno che non sia sicuro)
Method3() : sì

Method1 non può modificare foo e non ottiene mai un riferimento. Method2 ottiene un riferimento di breve durata a foo, che può utilizzare per modificare i campi di foo un numero qualsiasi di volte, in qualsiasi ordine, fino a quando non ritorna, ma non può persistere quel riferimento. Prima che il metodo 2 ritorni, a meno che non utilizzi 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 promiscuamente condivisibile a foo, e non si può dire cosa potrebbe farci. Potrebbe non cambiare affatto foo, potrebbe cambiare foo e poi tornare, oppure potrebbe dare un riferimento a foo a un altro thread che potrebbe mutarlo in qualche modo arbitrario in un futuro futuro arbitrario.

Le matrici di strutture offrono una meravigliosa semantica. Dato RectArray [500] di tipo Rectangle, è chiaro ed ovvio come ad esempio copiare l'elemento 123 nell'elemento 456 e poi qualche tempo dopo impostare la larghezza dell'elemento 123 su 555, senza disturbare l'elemento 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; ". Sapere che Rectangle è una struttura con un campo intero chiamato Larghezza ti dirà tutto ciò che devi sapere sulle affermazioni di cui sopra.

Supponiamo ora che RectClass fosse una classe con gli stessi campi di Rectangle e si volesse fare le stesse operazioni su un RectClassArray [500] di tipo RectClass. Forse l'array dovrebbe contenere 500 riferimenti immutabili preinizializzati a oggetti RectClass mutabili. in tal caso, il codice corretto sarebbe simile a "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Forse 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ù sia su RectClass (ad es. Supporta un costruttore di copie, un metodo di copia da, ecc. ) e l'uso previsto dell'array. Da nessuna parte pulito come usare una struttura.

A dire il vero, sfortunatamente non esiste un buon modo per qualsiasi classe di container diversa da un array di offrire la semantica pulita di un array di strutture. Il migliore che si potrebbe fare, se si volesse indicizzare una raccolta con ad esempio una stringa, sarebbe probabilmente quello di offrire un metodo "ActOnItem" generico che accetterebbe una stringa per l'indice, un parametro generico e un delegato che verrebbe passato per riferimento sia il parametro generico che l'elemento di raccolta. Ciò consentirebbe quasi la stessa semantica degli array di struttura, ma a meno che le persone vb.net e C # non possano essere perseguite per offrire una bella sintassi, il codice apparirà goffo anche se è ragionevolmente prestazioni (passare un parametro generico sarebbe consentire l'uso di un delegato statico ed evitare qualsiasi necessità di creare istanze di classe temporanee).

Personalmente, sono irritato dall'odio Eric Lippert et al. vomitare per quanto riguarda i tipi di valori mutabili. Offrono una semantica molto più pulita rispetto ai promiscui tipi di riferimento utilizzati ovunque. Nonostante alcune limitazioni con il supporto di .net per i tipi di valore, ci sono molti casi in cui i tipi di valori mutabili si adattano meglio di qualsiasi altro tipo di entità.


1
@Ron Warholic: non è evidente che SomeRect sia un rettangolo. Potrebbe essere un altro tipo che può essere implicitamente tipizzato da Rectangle. Tuttavia, l'unico tipo definito dal sistema che può essere implicitamente digitato da Rectangle è RectangleF, e il compilatore squawk se si provasse a passare i campi di un RectangleF al costruttore di Rectangle (dato che i primi sono Single e il secondo Intero) , potrebbero esistere strutture definite dall'utente che consentono tali tipi di carattere implicito. A proposito, la prima affermazione funzionerebbe ugualmente bene se SomeRect fosse un Rectangle o un RectangleF.
supercat,

Tutto ciò che hai mostrato è che in un esempio inventato credi che un metodo sia più chiaro. Se prendiamo il tuo esempio con, Rectanglepotrei facilmente trovare una posizione comune in cui si ottiene un comportamento molto poco chiaro . Si consideri che WinForms implementa un Rectangletipo mutabile utilizzato nella Boundsproprietà del modulo . Se voglio cambiare i limiti, vorrei usare la tua bella sintassi: form.Bounds.X = 10;tuttavia, questo non cambia esattamente nulla nel modulo (e genera un adorabile errore che ti informa di ciò). L'incoerenza è la rovina della programmazione ed è il motivo per cui è richiesta l'immutabilità.
Ron Warholic,

3
@Ron Warholic: A proposito, mi piace essere in grado di dire "form.Bounds.X = 10;" e farlo funzionare, ma il sistema non fornisce alcun modo pulito per farlo. Una convenzione per esporre le proprietà del tipo di valore come metodi che accettano callback potrebbe offrire un codice molto più pulito, efficiente e sicuramente corretto rispetto a qualsiasi approccio che utilizza le classi.
supercat,

4
Questa risposta è molto più approfondita di alcune delle risposte più votate. È assurdo che l'argomento contro i tipi di valori mutabili si basi sulla nozione di "cosa ti aspetti" che accada quando mescoli aliasing e mutazione. Questa è una cosa terribile da fare comunque !
Eamon Nerbonne,

2
@supercat: Chissà, forse quella funzione di ref-return di cui stanno parlando per C # 7 potrebbe coprire quella base (in realtà non l'ho vista in dettaglio, ma suona superficialmente simile).
Eamon Nerbonne,

24

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 poterlo modificare. 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 le stesse allora è la stessa rappresentazione universale di quel valore.

Come Konrad menziona, non ha nemmeno senso cambiare una data, poiché il valore rappresenta quel particolare punto nel tempo e non un'istanza di un oggetto tempo che ha uno stato o una dipendenza dal contesto.

Spero che questo abbia senso per te. Si tratta più del concetto che si tenta di acquisire con tipi di valore che di dettagli pratici, per essere sicuri.


Bene, dovrebbero rappresentare concetti immutabili, almeno ;-p
Marc Gravell

3
Bene, suppongo che avrebbero potuto rendere System.Drawing.Point immutabile ma sarebbe stato un grave errore di progettazione IMHO. Penso che i punti siano in realtà un tipo di valore archetipico e sono mutabili. E non causano alcun problema a nessuno al di fuori della programmazione 101 per principianti.
Stephen Martin,

3
In linea di principio, penso che anche i punti debbano essere immutabili, ma se rende il tipo più difficile o meno elegante da usare, ovviamente anche questo deve essere considerato. Non ha senso avere costrutti di codice che sostengano i migliori principi se nessuno vuole usarli;)
Morten Christiansen,

3
I tipi di valore sono utili per rappresentare semplici concetti immutabili, ma le strutture dei campi esposti sono i tipi migliori da utilizzare per contenere o passare piccoli insiemi fissi di valori correlati ma indipendenti (come le coordinate di un punto). Una posizione di archiviazione di tale tipo di valore incapsula i valori dei suoi campi e nient'altro. Al contrario, una posizione di memorizzazione di un tipo di riferimento mutabile può essere utilizzata allo scopo di contenere lo stato dell'oggetto mutabile, ma incapsula anche l'identità di tutti gli altri riferimenti nell'universo che esistono allo stesso oggetto.
supercat

4
"I tipi di valore rappresentano fondamentalmente concetti immutabili". No, non lo fanno. Una delle applicazioni più antiche e utili di una variabile con valori di valore è un intiteratore, che sarebbe completamente inutile se fosse immutabile. Penso che tu stia fondendo "implementazioni di compilatore / runtime di tipi di valore" con "variabili digitate in un tipo di valore" - quest'ultimo è certamente mutabile a uno qualsiasi dei possibili valori.
Slipp D. Thompson,

19

Ci sono un paio di altri casi angolari che potrebbero portare a comportamenti imprevedibili dal punto di vista del programmatore.

Tipi di valori 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);
        }
    }

Tipi di valori mutevoli e array

Supponiamo di avere una matrice della nostra Mutablestruttura e di chiamare il IncrementImetodo per il primo elemento di quella matrice. Che 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<T> interface
    IList<Mutable> 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 mutabili non sono malvagie fintanto che tu e il resto della squadra capite chiaramente cosa state facendo. Ma ci sono troppi casi angolari in cui il comportamento del programma sarebbe diverso da quello che ci si aspetta, che potrebbe portare a errori difficili da produrre e da capire.


5
Che cosa dovrebbe accadere, se i linguaggi .net avessero un supporto leggermente migliore di tipo valore, sarebbe che i metodi di struttura dovrebbero essere vietati dalla mutazione di "questo" a meno che non siano esplicitamente dichiarati come tali, e i metodi che sono così dichiarati dovrebbero essere vietati in sola lettura contesti. Le matrici di strutture mutabili offrono una semantica utile che non può essere raggiunta in modo efficiente con altri mezzi.
supercat,

2
questi sono buoni esempi di problemi molto sottili che potrebbero derivare da strutture mutabili. Non mi sarei aspettato questo comportamento. Perché un array dovrebbe darti un riferimento, ma un'interfaccia ti darebbe un valore? Avrei pensato, a parte i valori per tutto il tempo (che è quello che mi aspetterei davvero), che almeno sarebbe il contrario: interfaccia che fornisce riferimenti; array che danno valori ...
Dave Cousineau,

@Sahuagin: Sfortunatamente, non esiste un meccanismo standard attraverso il quale un'interfaccia può esporre un riferimento. Esistono modi in cui .net può consentire che tali operazioni vengano eseguite in modo sicuro e utile (ad esempio, definendo una speciale struttura "ArrayRef <T>" contenente a T[]e un indice intero e fornendo che un accesso a una proprietà di tipo ArrayRef<T>verrà interpretato come un accesso all'elemento array appropriato) [se una classe voleva esporre un ArrayRef<T>per qualsiasi altro scopo, potrebbe fornire un metodo - al contrario di una proprietà - per recuperarlo]. Sfortunatamente, non esistono tali disposizioni.
supercat

2
Oh mio ... questo rende le strutture mutevoli dannatamente malvagie!
nawfal,

1
Mi piace questa risposta perché contiene informazioni molto preziose che non sono ovvie. Ma davvero, questo non è un argomento contro le strutture mutabili, come alcuni sostengono. Sì, quello che vediamo qui è una "fossa di disperazione", come Eric l'avrebbe detto, ma la fonte di questa disperazione non è la mutabilità. La fonte della disperazione sono i metodi auto-mutanti delle strutture . (Per quanto riguarda il motivo per cui le matrici e gli elenchi si comportano in modo diverso è perché uno è fondamentalmente un operatore che calcola un indirizzo di memoria e l'altro è una proprietà. In generale tutto diventa chiaro una volta compreso che un "riferimento" è un valore di indirizzo .)
AnorZaken

18

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

Quindi, le strutture mutabili non sono malvagie, C # le ha rese malvagie. Uso sempre strutture mutabili 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.


Avere campi di classe dei tipi di struttura può spesso essere un modello molto utile, anche se è vero che ci sono alcune limitazioni. Le prestazioni saranno ridotte se si utilizzano proprietà anziché campi o usi readonly, ma se si evita di fare queste cose, i campi di classe dei tipi di struttura vanno bene. L'unica limitazione veramente fondamentale delle strutture è che un campo struct di un tipo di classe mutabile come int[]può incapsulare identità o un insieme immutabile di valori, ma non può essere usato per incapsulare valori mutabili senza incapsulare anche un'identità indesiderata.
supercat

13

Se ti attieni a ciò a cui sono destinate le strutture (in C #, Visual Basic 6, Pascal / Delphi, tipo di struttura C ++ (o classi) quando non sono usate come puntatori), scoprirai che una struttura non è più di una variabile composta . Questo significa: li tratterai come un insieme impacchettato di variabili, con un nome comune (una variabile record da cui fai riferimento ai membri).

So che confonderebbe molte persone profondamente abituate alla 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 di Pythonnamedtuple ), ma è un altro paradigma da considerare.

Sì: le strutture implicano 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 di 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 oggetto è la sua identità , che il più delle volte non è altro che il suo indirizzo di memoria. Struct sta per struttura dei dati (non un oggetto appropriato, quindi non hanno comunque identità) e i dati possono essere modificati. In altre lingue, record (invece di struct , come nel caso di Pascal) è la parola e ha lo stesso scopo: solo una variabile di record di dati, destinata a essere letta da file, modificata e scaricata in file (questo è il principale utilizzare e, in molte lingue, è anche possibile definire l'allineamento dei dati nel record, mentre ciò non è necessariamente il caso di oggetti correttamente chiamati).

Vuoi un buon esempio? Le strutture vengono utilizzate per leggere facilmente i file. Python ha questa libreria perché, poiché è orientata agli oggetti e non ha supporto per le strutture, ha dovuto implementarla in un altro modo, il che è piuttosto brutto. Le strutture che implementano le lingue hanno questa caratteristica ... integrata. Prova a leggere un'intestazione bitmap con una struttura appropriata in linguaggi come Pascal o C. Sarà facile (se la struttura è costruita e allineata correttamente; in Pascal non utilizzeresti un accesso basato su record ma funzioni per leggere dati binari arbitrari). Quindi, per i file e l'accesso diretto (locale) alla memoria, le strutture sono migliori degli oggetti. Ad oggi, siamo abituati a JSON e XML, e quindi dimentichiamo l'uso di file binari (e come effetto collaterale, l'uso di strutture). Ma sì: esistono e hanno uno scopo.

Non sono cattivi. Usali solo per lo scopo giusto.

Se pensi in termini di martelli, vorrai trattare le viti come chiodi, per scoprire che le viti sono più difficili da immergere nel muro, e sarà colpa delle viti, e saranno quelle malvagie.


12

Immagina di avere una matrice di 1.000.000 di strutture. Ogni struttura che rappresenta un'equità con elementi come bid_price, offer_price (forse decimali) e così via, questo viene creato da C # / VB.

Immagina 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 contemporaneamente all'array (forse un codice high-perf che fa matematica).

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

Immagina che ciò avvenga decine o addirittura centinaia di migliaia di volte al secondo.

Bene, affrontiamo i fatti, in questo caso vogliamo davvero che queste strutture siano mutabili, devono essere perché sono condivise da qualche altro codice nativo, quindi la creazione di copie non aiuta; devono esserlo perché fare una copia di una struttura di circa 120 byte a queste velocità è folle, specialmente quando un aggiornamento può effettivamente avere un impatto solo su uno o due byte.

Hugo


3
Vero, ma in questo caso la ragione per usare una struttura è che farlo è imposto alla progettazione dell'applicazione da vincoli esterni (quelli dall'uso del codice nativo). Tutto il resto che descrivi su questi oggetti suggerisce che dovrebbero essere chiaramente classi in C # o VB.NET.
Jon Hanna,

6
Non sono sicuro del motivo per cui alcune persone pensano che le cose dovrebbero essere oggetti di classe. Se tutti gli slot di array sono popolati con riferimenti istanze distinte, l'uso di un tipo di classe aggiungerà dodici o ventiquattro byte in più al requisito di memoria e l'accesso sequenziale su un array di riferimenti a oggetti di classe potrebbe essere molto più lento dell'accesso sequenziale su una serie di strutture.
supercat

9

Quando qualcosa può essere mutato, acquisisce un senso di identità.

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

Perché Personè mutevole, è più naturale pensare di cambiare la posizione di Eric che clonare Eric, spostare il clone e distruggere l'originale . Entrambe le operazioni sarebbero riuscite a modificare il contenuto dieric.position , ma una è più intuitiva dell'altra. Allo stesso modo, è più intuitivo passare Eric (come riferimento) per i metodi per modificarlo. Dare un metodo a un clone di Eric sarà quasi sempre sorprendente. Chiunque desideri mutare Persondeve ricordarsi di chiedere un riferimento Persono farà la cosa sbagliata.

Se si rende immutabile il tipo, il problema scompare; se non riesco a modificare eric, non fa differenza se ricevoeric o un clone di eric. Più in generale, un tipo è sicuro per passare per valore se tutto il suo stato osservabile è contenuto in membri che sono:

  • immutabile
  • tipi di riferimento
  • sicuro passare per valore

Se tali condizioni sono soddisfatte, un tipo di valore modificabile si comporta come un tipo di riferimento poiché una copia superficiale consentirà comunque al destinatario di modificare i dati originali.

L'intuitività di un immutabile Persondipende da cosa stai cercando di fare. Se Personrappresenta solo un insieme di dati su una persona, non c'è nulla di non intuitivo al riguardo; Personle variabili rappresentano veramente valori astratti , non oggetti. (In tal caso, probabilmente sarebbe più appropriato rinominarlo PersonData.) Se in Personrealtà modella una persona stessa, l'idea di creare e spostare costantemente cloni è sciocca anche se hai evitato la trappola di pensare che stai modificando l'originale. In tal caso, sarebbe probabilmente più naturale fare semplicementePerson un tipo di riferimento (ovvero una classe).

Certo, poiché la programmazione funzionale ci ha insegnato che ci sono vantaggi nel rendere tutto immutabile (nessuno può segretamente aggrapparsi a un riferimento erice mutarlo), ma dal momento che non è idiomatico in OOP non sarà ancora intuitivo per chiunque lavori con il tuo codice.


3
Il tuo punto sull'identità è buono; vale la pena notare che l'identità è rilevante solo quando esistono più riferimenti a qualcosa. Se foocontiene l'unico riferimento al suo obiettivo in qualsiasi parte dell'universo e nulla ha catturato il valore dell'hash di identità di quell'oggetto, allora il campo mutante foo.Xè semanticamente equivalente a fooindicare un nuovo oggetto che è proprio come quello a cui si riferiva in precedenza, ma con Xmantenendo il valore desiderato. Con i tipi di classe, è generalmente difficile sapere se esistono più riferimenti a qualcosa, ma con le strutture è facile: non lo fanno.
supercat

1
Se Thingè un tipo di classe mutevole, una Thing[] sarà incapsulare le identità degli oggetti - lo si voglia o no - a meno che non si può garantire che nessuno Thingnella matrice per cui esistono eventuali riferimenti esterni sarà mai mutato. Se non si desidera che gli elementi dell'array incapsulino l'identità, in genere è necessario assicurarsi che nessun elemento a cui contiene riferimenti sia mai mutato o che non esistano mai riferimenti esterni a elementi in possesso [gli approcci ibridi possono anche funzionare ]. Nessuno dei due approcci è terribilmente conveniente. Se Thingè una struttura, a Thing[]incapsula solo i valori.
supercat

Per gli oggetti, la loro identità viene dalla loro posizione. Le istanze dei tipi di riferimento hanno la loro identità grazie alla loro posizione nella memoria e si passa solo intorno alla loro identità (un riferimento), non ai loro dati, mentre i tipi di valore hanno la loro identità nel luogo esterno in cui sono memorizzati. L'identità del tipo di valore Eric proviene solo dalla variabile in cui è memorizzato. Se lo passi in giro, perderà la sua identità.
IllidanS4 vuole che Monica torni il

6

Non ha nulla a che fare con le strutture (e nemmeno con C #) ma in Java potresti avere problemi con oggetti mutabili quando sono, ad esempio, chiavi in ​​una mappa hash. Se li cambi dopo averli aggiunti a una mappa e cambia il suo codice hash , potrebbero accadere cose cattive.


3
Questo è vero se usi una classe anche come chiave in una mappa.
Marc Gravell

6

Personalmente, quando guardo il codice, mi sembra abbastanza goffo:

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

piuttosto che semplicemente

data.value ++; o data.value = data.value + 1;

L'incapsulamento dei dati è utile quando si passa una classe in giro e si desidera assicurarsi che il valore venga modificato in modo controllato. Tuttavia, quando si dispone di un set pubblico e si ottengono funzioni che fanno poco più che impostare il valore su ciò che viene mai passato, come può essere un miglioramento rispetto al semplice passaggio di una struttura di dati pubblici?

Quando creo una struttura privata all'interno di una classe, ho creato quella struttura per organizzare un insieme di variabili in un gruppo. Voglio essere in grado di modificare quella struttura nell'ambito della classe, non ottenere copie di quella struttura e creare nuove istanze.

Per me questo impedisce un uso valido delle strutture utilizzate per organizzare le variabili pubbliche, se volessi il controllo degli accessi userei una classe.


1
Dritto al punto! Le strutture sono unità organizzative senza restrizioni di controllo dell'accesso! Sfortunatamente, C # li ha resi inutili per questo scopo!
ThunderGr

questo manca completamente il punto poiché entrambi i tuoi esempi mostrano strutture mutabili.
vidstige,

C # li ha resi inutili a questo scopo perché non è quello lo scopo delle strutture
Luiz Felipe,

6

Ci sono diversi problemi nell'esempio di Eric Lippert. È inventato per illustrare il punto in cui le strutture vengono copiate e come ciò potrebbe costituire un problema se non si presta attenzione. Guardando l'esempio, lo vedo come conseguenza di una cattiva abitudine di programmazione e non di un vero problema con la struttura o la classe.

  1. Una struttura dovrebbe avere solo membri pubblici e non dovrebbe richiedere alcun incapsulamento. Se lo fa, allora dovrebbe davvero essere un tipo / classe. Non hai davvero bisogno di due costrutti per dire la stessa cosa.

  2. Se hai classe che racchiude una struttura, chiameresti un metodo nella classe per mutare la struttura membro. Questo è ciò che farei come una buona abitudine di programmazione.

Una corretta attuazione sarebbe la seguente.

struct Mutable {
public int x;
}

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

Sembra che sia un problema con l'abitudine alla programmazione piuttosto che un problema con la struttura stessa. Le strutture dovrebbero essere mutabili, questa è l'idea e l'intenzione.

Il risultato delle modifiche voila si comporta come previsto:

1 2 3 Premere un tasto qualsiasi per continuare. . .


4
Non c'è nulla di sbagliato nel progettare piccole strutture opache per comportarsi come oggetti di classe immutabili; le linee guida MSDN sono ragionevoli quando si cerca di creare qualcosa che si comporti come un oggetto . Le strutture sono appropriate in alcuni casi in cui sono necessarie cose leggere che si comportano come oggetti e in casi in cui è necessario un mucchio di variabili incollate insieme al nastro adesivo. Per qualche ragione, tuttavia, molte persone non riescono a rendersi conto che le strutture hanno due usi distinti e che le linee guida appropriate per l'una sono inadeguate per l'altra.
supercat,

5

Ci sono molti vantaggi e svantaggi per i dati mutabili. Lo svantaggio di un milione di dollari è l'aliasing. Se lo stesso valore viene utilizzato in più punti e uno di essi lo modifica, sembrerà che sia magicamente cambiato negli altri luoghi che lo utilizzano. Ciò è legato, ma non identico, alle condizioni di gara.

Il vantaggio di milioni di dollari è la modularità, a volte. Lo stato mutevole può consentire di nascondere le informazioni modificabili dal codice che non è necessario conoscere.

L'Arte dell'Interprete approfondisce questi dettagli in alcuni dettagli e fornisce alcuni esempi.


le strutture non sono modificabili in c #. Ogni assegnazione di una struttura è una copia.
ricorsivo

@recursive: in alcuni casi, questo è un grande vantaggio delle strutture mutabili, e uno che mi fa mettere in dubbio l'idea che le strutture non dovrebbero essere mutabili. Il fatto che i compilatori talvolta copiano implicitamente le strutture non riduce l'utilità delle strutture mutabili.
supercat

1

Non credo che siano cattivi se usati correttamente. Non lo inserirò nel mio codice di produzione, ma preferirei qualcosa come simulazioni di test di unità strutturate, in cui la durata di una struttura è relativamente piccola.

Usando l'esempio Eric, forse vuoi creare una seconda istanza di quell' Eric, ma apportare modifiche, poiché questa è la natura del tuo test (cioè duplicazione, quindi modifica). Non importa cosa succede con la prima istanza di Eric se stiamo solo usando Eric2 per il resto dello script di test, a meno che tu non stia pianificando di usarlo come confronto di test.

Ciò sarebbe utile soprattutto per testare o modificare il codice legacy che shallow definisce un particolare oggetto (il punto delle strutture), ma avendo una struttura immutabile, questo ne impedisce l'uso in modo fastidioso.


A mio modo di vedere, una struttura è nel suo insieme un insieme di variabili attaccate insieme al nastro adesivo. In .NET è possibile che una struttura finga di essere qualcosa di diverso da un mucchio di variabili attaccate insieme al nastro adesivo, e suggerirei che quando pratico un tipo che fingerà di essere qualcosa di diverso da un mucchio di variabili attaccate insieme con il nastro adesivo dovrebbe comportarsi come un oggetto unificato (che per una struttura implicherebbe immutabilità), ma a volte è utile incollare un mucchio di variabili insieme al nastro adesivo. Anche nel codice di produzione, considererei meglio avere un tipo ...
supercat

... che chiaramente non ha semantiche oltre "ogni campo contiene l'ultima cosa scritta su di esso", spingendo tutta la semantica nel codice che utilizza la struttura, piuttosto che cercare di avere una struttura che faccia di più. Dato, ad esempio, un Range<T>tipo con membri Minimume Maximumcampi di tipo Te codice Range<double> myRange = foo.getRange();, qualsiasi garanzia su cosa Minimume cosa Maximumdovrebbe provenire foo.GetRange();. Avere Rangeuna struttura a campo esposto chiarirebbe che non aggiungerà alcun comportamento a sé stante.
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.