Quali sono gli svantaggi di tipi immutabili?


12

Mi vedo usare tipi sempre più immutabili quando non si prevede che le istanze della classe vengano modificate . Richiede più lavoro (vedere l'esempio seguente), ma semplifica l'utilizzo dei tipi in un ambiente multithread.

Allo stesso tempo, raramente vedo tipi immutabili in altre applicazioni, anche quando la mutabilità non gioverebbe a nessuno.

Domanda: Perché i tipi immutabili sono così raramente utilizzati in altre applicazioni?

  • È perché è più lungo scrivere codice per un tipo immutabile,
  • O mi sto perdendo qualcosa e ci sono alcuni svantaggi importanti quando si usano tipi immutabili?

Esempio dalla vita reale

Supponiamo che tu ottenga Weatherda un'API RESTful del genere:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

Quello che generalmente vedremmo è (nuove righe e commenti rimossi per abbreviare il codice):

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

D'altra parte, lo scriverei in questo modo, dato che ottenere un WeatherAPI dall'API, quindi modificarlo sarebbe strano: cambiare Temperatureo Skynon cambierebbe il tempo nel mondo reale, e cambiare CorrespondingCitynon ha nemmeno senso.

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

3
"Richiede più lavoro" - citazione richiesta. Nella mia esperienza richiede meno lavoro.
Konrad Rudolph,

1
@KonradRudolph: per più lavoro , intendo più codice da scrivere per creare una classe immutabile. L'esempio della mia domanda illustra questo, con 7 righe per una classe mutabile e 19 per una classe immutabile.
Arseni Mourzenko,

È possibile ridurre la digitazione del codice utilizzando la funzionalità Snippet di codice in Visual Studio se la si utilizza. Puoi creare i tuoi snippet personalizzati e lasciare che l'IDE ti definisca il campo e la proprietà contemporaneamente con poche chiavi. I tipi immutabili sono essenziali per il multithreading e sono ampiamente utilizzati in lingue come Scala.
Mert Akcakaya,

@Mert: i frammenti di codice sono ottimi per cose semplici. Scrivere uno snippet di codice che creerà una classe completa con commenti su campi e proprietà e un corretto ordinamento non sarebbe un compito facile.
Arseni Mourzenko,

5
Non sono d'accordo con l'esempio dato, la versione immutabile sta facendo più cose diverse. Potresti rimuovere le variabili a livello di istanza dichiarando le proprietà con gli accessori {get; private set;}, e anche quella mutabile dovrebbe avere un costruttore, perché tutti questi campi dovrebbero essere sempre impostati e perché non lo imporresti? Apportare questi due cambiamenti perfettamente ragionevoli li porta alla funzionalità e alla parità LoC.
Phoshi,

Risposte:


16

Programma in C # e Objective-C. Mi piace molto la digitazione immutabile, ma nella vita reale sono sempre stato costretto a limitarne l'utilizzo, principalmente per i tipi di dati, per i seguenti motivi:

  1. Sforzo di implementazione rispetto ai tipi mutabili. Con un tipo immutabile, è necessario disporre di un costruttore che richieda argomenti per tutte le proprietà. Il tuo esempio è buono. Prova a immaginare di avere 10 classi, ognuna con 5-10 proprietà. Per semplificare le cose, potrebbe essere necessario disporre di una classe builder per costruire o creare istanze immutabili modificate in modo simile a StringBuildero UriBuilderin C # o WeatherBuildernel tuo caso. Questa è la ragione principale per me dal momento che molte classi che disegno non valgono uno sforzo del genere.
  2. Usabilità del consumatore . Un tipo immutabile è più difficile da usare rispetto al tipo mutabile. L'istanza richiede l'inizializzazione di tutti i valori. Immutabilità significa anche che non possiamo passare l'istanza a un metodo per modificarne il valore senza usare un builder e se abbiamo bisogno di un builder, lo svantaggio è nel mio (1).
  3. Compatibilità con il framework del linguaggio. Molti framework di dati richiedono tipi mutabili e costruttori predefiniti per funzionare. Ad esempio, non è possibile eseguire query LINQ-to-SQL nidificate con tipi immutabili e non è possibile associare proprietà da modificare in editor come TextBox di Windows Form.

In breve, l'immutabilità è buona per gli oggetti che si comportano come valori o hanno solo poche proprietà. Prima di rendere qualsiasi cosa immutabile, devi considerare lo sforzo necessario e l'usabilità della classe stessa dopo averla resa immutabile.


11
"L'istanza ha bisogno di tutti i valori in anticipo.": Anche un tipo mutabile fa, a meno che tu non accetti il ​​rischio di avere un oggetto con campi non inizializzati che fluttua intorno ...
Giorgio,

@Giorgio Per il tipo modificabile, il costruttore predefinito dovrebbe inizializzare l'istanza allo stato predefinito e lo stato dell'istanza può essere modificato in seguito dopo l'istanza.
tia,

8
Per un tipo immutabile puoi avere lo stesso costruttore predefinito e crearne una copia in seguito usando un altro costruttore. Se i valori predefiniti sono validi per il tipo modificabile, dovrebbero essere validi anche per il tipo immutabile perché in entrambi i casi si sta modellando la stessa entità. O qual è la differenza?
Giorgio,

1
Un'altra cosa da considerare è ciò che rappresenta il tipo. I contratti di dati non rendono buoni tipi immutabili a causa di tutti questi punti, ma i tipi di servizi che vengono inizializzati con dipendenze o leggono solo i dati e quindi eseguono operazioni sono ottimi per l'immutabilità perché le operazioni verranno eseguite in modo coerente e lo stato del servizio non può essere cambiato per rischiare che.
Kevin,

1
Attualmente codice in F #, dove l'immutabilità è l'impostazione predefinita (quindi più facile da implementare). Trovo che il tuo punto 3 sia il grande ostacolo. Non appena usi la maggior parte delle librerie .Net standard, dovrai saltare attraverso i cerchi. (E se usano la riflessione e quindi aggirano l'immutabilità ... argh!)
Guran,

5

Solo i tipi generalmente immutabili creati in linguaggi che non ruotano attorno all'immutabilità tenderanno a costare più tempo per lo sviluppo degli sviluppatori e potrebbero essere potenzialmente utilizzati se richiedono un tipo di oggetto "costruttore" per esprimere le modifiche desiderate (ciò non significa che il il lavoro sarà maggiore, ma in questi casi c'è un costo iniziale. Inoltre, indipendentemente dal fatto che la lingua renda davvero semplice la creazione di tipi immutabili o meno, tenderà a richiedere sempre un po 'di elaborazione e sovraccarico di memoria per tipi di dati non banali.

Rendere le funzioni prive di effetti collaterali

Se stai lavorando in lingue che non ruotano attorno all'immutabilità, penso che l'approccio pragmatico non sia quello di cercare di rendere immutabile ogni singolo tipo di dati. Una mentalità potenzialmente molto più produttiva che offre molti degli stessi vantaggi è quella di concentrarsi sulla massimizzazione del numero di funzioni nel sistema che causano zero effetti collaterali .

Ad esempio, se hai una funzione che provoca un effetto collaterale come questo:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

Quindi non abbiamo bisogno di un tipo di dati integer immutabile che proibisca agli operatori come l'assegnazione post-inizializzazione di evitare che la funzione eviti effetti collaterali. Possiamo semplicemente farlo:

// Returns the absolute value of 'x'.
int abs(int x);

Ora la funzione non interferisce con xnulla al di fuori del suo ambito di applicazione, e in questo banale caso potremmo anche aver rasato alcuni cicli evitando qualsiasi sovraccarico associato all'indirizzamento / aliasing. Almeno la seconda versione non dovrebbe essere più computazionalmente costosa della prima.

Cose che sono costose da copiare per intero

Naturalmente la maggior parte dei casi non è così banale se vogliamo evitare che una funzione causi effetti collaterali. Un caso d'uso complesso e complesso potrebbe essere più simile a questo:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

A quel punto la mesh potrebbe richiedere circa duecento megabyte di memoria con oltre centomila poligoni, ancora più vertici e spigoli, mappe di texture multiple, target di morph, ecc. Sarebbe davvero costoso copiare l'intera mesh solo per fare questo transformsenza effetti collaterali, in questo modo:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

Ed è in questi casi in cui la copia di qualcosa nella sua interezza sarebbe normalmente un sovraccarico epico in cui ho trovato utile trasformarsi Meshin una struttura di dati persistente e un tipo immutabile con il "builder" analogico per creare versioni modificate di esso in modo che può semplicemente copiare parti di conteggio e riferimento non contigue. È tutto incentrato sulla capacità di scrivere funzioni mesh che sono prive di effetti collaterali.

Strutture di dati persistenti

E in questi casi in cui copiare tutto è così incredibilmente costoso, ho trovato lo sforzo di progettare un modello immutabile Meshper pagare davvero, anche se aveva un costo leggermente elevato in anticipo, perché non semplificava solo la sicurezza del thread. Ha inoltre semplificato la modifica non distruttiva (che consente all'utente di sovrapporre le operazioni di mesh senza modificare la sua copia originale), di annullare i sistemi (ora il sistema di annullamento può semplicemente archiviare una copia immutabile della mesh prima delle modifiche apportate da un'operazione senza far saltare la memoria uso) e sicurezza delle eccezioni (ora se si verifica un'eccezione nella funzione sopra, la funzione non deve tornare indietro e annullare tutti i suoi effetti collaterali poiché non ha causato alcun inizio).

Posso affermare con certezza in questi casi che il tempo necessario per rendere immutabili queste strutture di dati pesanti ha permesso di risparmiare più tempo di quanto non sia costato, poiché ho confrontato i costi di manutenzione di questi nuovi progetti con quelli precedenti che ruotavano attorno alla mutabilità e alle funzioni che causavano effetti collaterali, e i precedenti progetti mutabili costano molto più tempo ed erano molto più inclini all'errore umano, specialmente in aree che sono davvero allettanti per gli sviluppatori da trascurare durante il periodo di crisi, come la sicurezza delle eccezioni.

Quindi penso che in questi casi i tipi di dati immutabili paghino davvero, ma non tutto deve essere reso immutabile per rendere la maggior parte delle funzioni del sistema prive di effetti collaterali. Molte cose sono abbastanza economiche da copiarle per intero. Inoltre, molte applicazioni del mondo reale dovranno causare alcuni effetti collaterali qua e là (almeno come il salvataggio di un file), ma in genere ci sono molte più funzioni che potrebbero essere prive di effetti collaterali.

Il punto di avere alcuni tipi di dati immutabili per me è assicurarmi di poter scrivere il numero massimo di funzioni per essere liberi da effetti collaterali senza incorrere in spese generali epiche sotto forma di copia profonda di enormi strutture di dati a sinistra ea destra per intero quando solo piccole porzioni di loro devono essere modificati. Avere strutture di dati persistenti in questi casi finisce per diventare un dettaglio di ottimizzazione per permetterci di scrivere le nostre funzioni in modo da evitare effetti collaterali senza pagare un costo epico per farlo.

Immutabile sovraccarico

Ora concettualmente le versioni mutabili avranno sempre un vantaggio in termini di efficienza. C'è sempre quel sovraccarico computazionale associato a strutture di dati immutabili. Ma l'ho trovato uno scambio degno nei casi che ho descritto sopra, e puoi concentrarti sul rendere il sovraccarico di natura sufficientemente minimale. Preferisco quel tipo di approccio in cui la correttezza diventa facile e l'ottimizzazione diventa più difficile piuttosto che l'ottimizzazione diventa più facile, ma la correttezza diventa più difficile. Non è altrettanto demoralizzante avere un codice che funzioni perfettamente correttamente e necessiti di qualche messa a punto in più sul codice che non funziona correttamente in primo luogo, non importa quanto velocemente raggiunga i suoi risultati errati.


3

L'unico inconveniente a cui riesco a pensare è che in teoria l'uso di dati immutabili può essere più lento di quelli mutabili: è più lento creare una nuova istanza e raccogliere quella precedente piuttosto che modificare quella esistente.

L'altro "problema" è che non puoi usare solo tipi immutabili. Alla fine devi descrivere lo stato e devi usare tipi mutabili per farlo - senza cambiare stato non puoi fare alcun lavoro.

Ma la regola generale è quella di utilizzare tipi immutabili ovunque sia possibile e renderli mutabili solo quando c'è davvero un motivo per farlo ...

E per rispondere alla domanda " Perché i tipi immutabili sono così raramente utilizzati in altre applicazioni? " - Non credo proprio che siano ... ovunque tu guardi, tutti raccomandano di rendere le tue lezioni immutabili come possono essere ... per esempio: http://www.javapractices.com/topic/TopicAction.do?Id=29


1
Entrambi i tuoi problemi non sono in Haskell però.
Florian Margaine,

@FlorianMargaine Potresti elaborare?
mppyo,

La lentezza non è vera grazie a un compilatore intelligente. E in Haskell, anche l'I / O è tramite un'API immutabile.
Florian Margaine,

2
Un problema più fondamentale della velocità è che per gli oggetti immutabili è difficile mantenere un'identità mentre il loro stato cambia. Se un Caroggetto mutabile viene continuamente aggiornato con la posizione di una particolare automobile fisica, allora se ho un riferimento a quell'oggetto posso scoprire dove si trova quell'automobile rapidamente e facilmente. Se Carfosse immutabile, probabilmente trovare la posizione attuale sarebbe molto più difficile.
supercat

A volte è necessario programmare in modo abbastanza intelligente per consentire al compilatore di capire che non vi è alcun riferimento all'oggetto precedente lasciato indietro e quindi può modificarlo in posizione o eseguire trasformazioni di deforestazione, ecc. Soprattutto nei programmi più grandi. E come dice @supercat, l'identità può davvero diventare un problema.
Macke,

0

Per modellare qualsiasi sistema del mondo reale in cui le cose possono cambiare, lo stato mutevole dovrà essere codificato da qualche parte, in qualche modo. Esistono tre modi principali in cui un oggetto può contenere uno stato mutabile:

  • Utilizzo di un riferimento mutabile a un oggetto immutabile
  • Utilizzo di un riferimento immutabile a un oggetto mutabile
  • Utilizzo di un riferimento modificabile a un oggetto mutabile

L'uso di first rende facile per un oggetto creare un'istantanea immutabile dello stato attuale. L'uso del secondo semplifica la creazione di una vista live dello stato attuale da parte di un oggetto. L'utilizzo del terzo può talvolta rendere alcune azioni più efficienti nei casi in cui c'è poco bisogno di istantanee immutabili o viste dal vivo.

Oltre al fatto che l'aggiornamento dello stato memorizzato utilizzando un riferimento mutabile a un oggetto immutabile è spesso più lento dell'aggiornamento dello stato memorizzato utilizzando un oggetto mutabile, l'utilizzo di un riferimento mutabile richiederà di rinunciare alla possibilità di costruire una vista live economica dello stato. Se non è necessario creare una vista dal vivo, questo non è un problema; se, tuttavia, fosse necessario creare una vista dal vivo, l'incapacità di utilizzare un riferimento immutabile effettuerà tutte le operazioni con la vista, sia in lettura che in scrittura- molto più lento di quanto sarebbero altrimenti. Se la necessità di istantanee immutabili supera la necessità di viste dal vivo, il miglioramento delle prestazioni di istantanee immutabili può giustificare l'hit di prestazione per le viste dal vivo, ma se si hanno bisogno di viste dal vivo e non sono necessarie istantanee, usare riferimenti immutabili ad oggetti mutabili è il modo in cui andare.


0

Nel tuo caso la risposta è principalmente perché C # ha uno scarso supporto per l'Immutabilità ...

Sarebbe bello se:

  • tutto sarà immutabile per impostazione predefinita se non diversamente specificato (ovvero con una parola chiave "mutabile"), mescolare tipi immutabili e mutabili è fonte di confusione

  • i metodi di mutazione ( With) saranno automaticamente disponibili, anche se questo può essere già ottenuto vedere Con

  • ci sarà un modo per dire che il risultato di una specifica chiamata di metodo (cioè ImmutableList<T>.Add) non può essere scartato o almeno produrrà un avviso

  • E soprattutto se il compilatore potesse, per quanto possibile, garantire l'immutabilità ove richiesto (consultare https://github.com/dotnet/roslyn/issues/159 )


1
Per quanto riguarda il terzo punto, ReSharper ha un MustUseReturnValueAttributeattributo personalizzato che fa esattamente questo. PureAttributeha lo stesso effetto ed è ancora meglio per questo.
Sebastian Redl,

-1

Perché i tipi immutabili sono così raramente utilizzati in altre applicazioni?

Ignoranza? Inesperienza?

Gli oggetti immutabili sono oggi ampiamente considerati superiori, ma è uno sviluppo relativamente recente. Gli ingegneri che non si sono tenuti aggiornati o sono semplicemente bloccati in "ciò che sanno" non li useranno. E ci vogliono un po 'di modifiche al design per usarle in modo efficace. Se le app sono vecchie o gli ingegneri sono deboli nelle capacità di progettazione, usarle potrebbe essere scomodo o altrimenti problematico.


"Ingegneri che non si sono tenuti aggiornati": si potrebbe dire che un ingegnere dovrebbe anche conoscere le tecnologie non tradizionali. L'idea di immutabilità sta diventando solo di recente mainstream, ma è un'idea piuttosto vecchia ed è supportata (se non applicata) da linguaggi più vecchi come Scheme, SML, Haskell. Quindi chiunque fosse abituato a guardare oltre le lingue tradizionali avrebbe potuto impararlo anche 30 anni fa.
Giorgio,

@Giorgio: in alcuni paesi, molti ingegneri scrivono ancora codice C # senza LINQ, senza FP, senza iteratori e senza generici, quindi in realtà in qualche modo hanno perso tutto quello che è successo con C # dal 2003. Se non conoscono nemmeno la loro lingua di preferenza , Quasi non li immagino che conoscano un linguaggio non tradizionale.
Arseni Mourzenko,

@MainMa: Bene, hai scritto la parola ingegneri in corsivo.
Giorgio,

@Giorgio: nel mio paese, sono anche chiamati architetti , consulenti e molti altri termini vanagloriosi, mai scritti in corsivo. Nella società in cui sto attualmente lavorando, mi chiamo sviluppatore di analisti e mi aspetto di trascorrere il mio tempo a scrivere CSS per codice HTML legacy scadente. I titoli di lavoro sono inquietanti su così tanti livelli.
Arseni Mourzenko,

1
@MainMa: sono d'accordo. Titoli come ingegnere o consulente sono spesso solo parole d'ordine senza un significato ampiamente accettato. Sono spesso usati per rendere qualcuno o la sua posizione più importante / prestigiosa di quanto non sia in realtà.
Giorgio,
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.