Domanda

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

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

È stato utile?

Soluzione

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 in giro.

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

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

Altri suggerimenti

Da dove iniziare ;-p

Blog di Eric Lippert è sempre buono per un preventivo:

  

Questo è un altro motivo per cui è mutevole   i tipi di valore sono cattivi. Prova sempre   rendere immutabili i tipi di valore.

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

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 farlo:

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 una struttura con due int , una stringa , un DateTime e un bool , puoi brucia molto velocemente attraverso molta memoria. Con una classe, più chiamanti possono condividere un riferimento alla stessa istanza (i riferimenti sono piccoli).

Non direi 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 di ciò sono i 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 effettiva necessario , il programmatore pensa 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 a basso costo, quindi le prestazioni non sono una considerazione.

Le strutture mutabili 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 chiamerei l'uso di una struttura immutabile in questi casi d'uso perfettamente validi "male".

Riesco a capire 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 preferendo le strutture immutabili, che impongono l'immutabilità , su strutture mutabili.

Tuttavia, invece di etichettare semplicemente le strutture immutabili come "male", 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. hai bisogno di un riferimento se non vuoi copiarli " o " prova a lavorare con le strutture di sola lettura prima " .

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

Metodi strutturali (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. Metodi Struct che non mutano "questo" dovrebbe essere invocabile anche su strutture di sola lettura senza necessità di copia difensiva. Metodi che mutano "questo" non dovrebbe essere invocabile affatto su strutture di sola lettura. Poiché .net non vuole vietare metodi di strutturazione che non modificano " questo " dall'essere invocato su strutture di sola lettura, ma non vuole permettere che le strutture di sola lettura vengano mutate, copia difensivamente le strutture in contesti di sola lettura, ottenendo probabilmente il peggio di entrambi i mondi.

Nonostante i problemi con la gestione dei metodi di auto-mutazione in contesti di sola lettura, tuttavia, le strutture mutabili offrono spesso una semantica molto 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 alcuna " non sicuro " codice, 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:
 & # 8195; Metodo1 () : no (intento chiaro)
 & # 8195; Method2 () : yes (intento chiaro)
 & # 8195; Method3 () : yes (intento incerto)
 Domanda 2:
 & # 8195; Metodo1 () : no
 & # 8195; Metodo2 () : no (a meno che non sia sicuro)
 & # 8195; Method3 () : yes

Metodo1 non può modificare foo e non ottiene mai un riferimento. Method2 ottiene un riferimento di breve durata a foo, che può usare 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. L'unico modo per limitare ciò che Method3 potrebbe fare a un oggetto di classe mutabile passato in esso sarebbe quello di incapsulare l'oggetto mutabile in un wrapper di sola lettura, che è brutto e ingombrante.

Le matrici di strutture offrono una meravigliosa semantica. Dato RectArray [500] di tipo Rectangle, è chiaro ed ovvio come ad es. copia l'elemento 123 nell'elemento 456 e poi qualche tempo dopo imposta 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.

Ora supponiamo 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 ad 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ù su RectClass (e.

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 menziona Konrad, non ha nemmeno senso cambiare una data, poiché il valore rappresenta quel punto nel tempo univoco e non un'istanza di un oggetto temporale che ha una dipendenza dallo stato o 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.

Esistono altri casi angolari che potrebbero comportare comportamenti imprevedibili dal punto di vista dei programmatori. Eccone un paio.

  1. 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);
    }
}

  1. Tipi e array di valori mutabili

Supponiamo di avere una matrice della nostra struttura mutabile e stiamo chiamando il metodo IncrementI 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 atteso, il che potrebbe portare a errori difficili da produrre e da capire.

Se hai mai programmato in una lingua 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 cattive, C # le ha rese cattive. 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.

Immagina di avere un array 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 120 byte a queste velocità è folle, specialmente quando un aggiornamento può avere un impatto su solo un byte o due.

Hugo

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, sotto un nome comune (una variabile record da cui fai riferimento ai membri).

So che confonderebbe molte 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 del namedtuple di Python), 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 (anziché struct , come nel caso di Pascal) è la parola e ha lo stesso scopo: solo una variabile di record di dati, che deve essere letta da file, modificati e scaricati in file (questo è l'uso principale 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 implementarlo in un altro modo, il che è piuttosto brutto. Le strutture che implementano le lingue hanno questa funzione ... 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 meglio 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.

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

Poiché Person è mutabile, è più naturale pensare a cambiare la posizione di Eric che a clonare Eric, spostare il clone e distruggere l'originale . Entrambe le operazioni riuscirebbero a cambiare il contenuto di eric.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 Person deve ricordarsi di chiedere un riferimento a Person o faranno la cosa sbagliata.

Se si rende immutabile il tipo, il problema scompare; se non riesco a modificare eric , non fa differenza se ricevo eric 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 da 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 una Persona immutabile dipende da cosa stai cercando di fare. Se Person rappresenta solo un set di dati su una persona, non c'è nulla di non intuitivo al riguardo; Le variabili Person rappresentano veramente valori astratti, non oggetti. (In tal caso, sarebbe probabilmente più appropriato rinominarlo in PersonData .) Se Person sta effettivamente modellando una persona stessa, l'idea di creare e spostare costantemente cloni è sciocco anche se hai evitato la trappola di pensare che stai modificando l'originale. In tal caso, sarebbe probabilmente più naturale rendere Person un tipo di riferimento (ovvero una classe)

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

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 es. chiavi in ??una mappa hash. Se le modifichi dopo averle aggiunte a una mappa e cambia codice hash , potrebbero accadere cose cattive.

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 ben poco più che impostare il valore su ciò che viene passato, in che modo si tratta di 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.

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 una classe che racchiude una struttura, chiameresti un metodo nella classe per mutare la struttura del membro. Questo è ciò che farei come una buona abitudine di programmazione.

Una corretta implementazione 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 . . .

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ò è correlato, 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 modificanti dal codice che non è necessario conoscere.

L'arte dell'interprete approfondisce questi dettagli in alcuni dettagli e fornisce alcuni esempi.

Non credo che siano malvagi 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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top