Quindi a che punto una classe diventa troppo complessa per essere immutabile?
Secondo me non vale la pena preoccuparsi di rendere immutabili le piccole classi in lingue come quella che stai mostrando. Sto usando piccoli qui e non complessi , perché anche se aggiungi dieci campi a quella classe e fa davvero operazioni fantasiose su di essi, dubito che prenderà i kilobyte per non parlare dei megabyte per non parlare dei gigabyte, quindi qualsiasi funzione che utilizza istanze del tuo class può semplicemente fare una copia economica dell'intero oggetto per evitare di modificare l'originale se vuole evitare di causare effetti collaterali esterni.
Strutture di dati persistenti
Dove trovo un uso personale per l'immutabilità è per strutture di dati centrali e di grandi dimensioni che aggregano un mucchio di dati di adolescenti come istanze della classe che stai mostrando, come una che ne memorizza un milione NamedThings
. Appartenendo a una struttura di dati persistente che è immutabile e si trova dietro un'interfaccia che consente solo l'accesso in sola lettura, gli elementi che appartengono al contenitore diventano immutabili senza che la classe element ( NamedThing
) debba occuparsene.
Copie economiche
La struttura dei dati persistente consente alle regioni di essere trasformate e rese uniche, evitando modifiche all'originale senza dover copiare la struttura dei dati nella sua interezza. Questa è la vera bellezza. Se volessi scrivere in modo ingenuo funzioni che evitino effetti collaterali che introducono una struttura di dati che richiede gigabyte di memoria e modifica solo la memoria di un megabyte, allora dovresti copiare l'intera cosa spaventosa per evitare di toccare l'input e restituire un nuovo produzione. O copia gigabyte per evitare effetti collaterali o causare effetti collaterali in quello scenario, facendoti scegliere tra due scelte spiacevoli.
Con una struttura di dati persistente, ti permette di scrivere una tale funzione ed evitare di fare una copia dell'intera struttura di dati, richiedendo solo un megabyte di memoria aggiuntiva per l'output se la tua funzione ha trasformato solo un valore di memoria di un megabyte.
fardello
Per quanto riguarda l'onere, ce n'è uno immediato almeno nel mio caso. Ho bisogno di quei costruttori di cui la gente parla o "transitori" come li chiamo per essere in grado di esprimere efficacemente le trasformazioni di quella massiccia struttura di dati senza toccarlo. Codice come questo:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... quindi deve essere scritto in questo modo:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
Ma in cambio di quelle due righe di codice in più, la funzione è ora sicura per chiamare attraverso thread con lo stesso elenco originale, non provoca effetti collaterali, ecc. Inoltre rende molto semplice rendere questa operazione un'azione dell'utente annullabile dal annulla può semplicemente archiviare una copia superficiale economica della vecchia lista.
Eccezione per la sicurezza o il recupero degli errori
Non tutti potrebbero trarre beneficio tanto quanto ho fatto dalle strutture di dati persistenti in contesti come questi (ho trovato un grande uso per loro nei sistemi di annullamento e nella modifica non distruttiva che sono concetti centrali nel mio dominio VFX), ma una cosa applicabile a quasi tutti da considerare sono la sicurezza delle eccezioni o il recupero degli errori .
Se si desidera rendere la funzione di muting originale sicura per le eccezioni, è necessaria una logica di rollback, per la quale l'implementazione più semplice richiede la copia dell'intero elenco:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
A questo punto la versione mutabile a prova di eccezione è ancora più computazionalmente costosa e probabilmente anche più difficile da scrivere correttamente rispetto alla versione immutabile che utilizza un "builder". E molti sviluppatori C ++ trascurano semplicemente la sicurezza delle eccezioni e forse va bene per il loro dominio, ma nel mio caso mi piace assicurarmi che il mio codice funzioni correttamente anche in caso di un'eccezione (anche scrivendo test che deliberatamente generano eccezioni per testare l'eccezione sicurezza), e questo lo rende quindi devo essere in grado di ripristinare qualsiasi effetto collaterale che una funzione provoca a metà della funzione se qualcosa viene lanciato.
Quando si desidera essere eccezionalmente sicuri e ripristinare gli errori con garbo senza che l'applicazione si blocchi e venga masterizzata, è necessario ripristinare / annullare gli effetti collaterali che una funzione può causare in caso di errore / eccezione. E lì il costruttore può effettivamente risparmiare più tempo del programmatore di quanto costa insieme al tempo di calcolo perché: ...
Non devi preoccuparti del rollback degli effetti collaterali in una funzione che non provoca alcun effetto!
Quindi torniamo alla domanda fondamentale:
A che punto le classi immutabili diventano un peso?
Sono sempre un peso per le lingue che ruotano più sulla mutabilità che sull'immutabilità, motivo per cui penso che dovresti usarle laddove i benefici superano significativamente i costi. Ma a un livello sufficientemente ampio per strutture di dati abbastanza grandi, credo che ci siano molti casi in cui è un degno compromesso.
Anche nel mio, ho solo alcuni tipi di dati immutabili, e sono tutte enormi strutture di dati destinate a memorizzare un numero enorme di elementi (pixel di un'immagine / trama, entità e componenti di un ECS e vertici / spigoli / poligoni di una maglia).