So at what point does a class become too complex to be immutable?
Secondo me non vale la pena preoccuparsi di rendere le classi piccole immutabili in lingue come quella che stai mostrando. Sto usando piccolo qui e non complesso , perché anche se aggiungi dieci campi a quella classe e sono davvero fantasiose operazioni su di essi, dubito che prenderà kilobyte per non parlare dei megabytes, tanto meno dei gigabyte, quindi qualsiasi funzione che usi le istanze della tua classe può semplicemente creare una copia economica dell'intero oggetto per evitare di modificare l'originale se vuole evitare di causare effetti collaterali esterni.
Strutture dati persistenti
Dove trovo l'uso personale per l'immutabilità è per le grandi strutture dati centrali che aggregano un mucchio di dati teeny come le istanze della classe che stai mostrando, come quella che memorizza un milione di% diNamedThings
. Appartenendo a una struttura dati persistente che è immutabile e che sta dietro un'interfaccia che consente solo l'accesso di sola lettura, gli elementi che appartengono al contenitore diventano immutabili senza che la classe dell'elemento ( NamedThing
) debba occuparsene.
Copie economiche
La struttura persistente dei dati consente di trasformare e rendere uniche le regioni, evitando modifiche all'originale senza dover copiare la struttura dei dati nella sua interezza. Questa è la vera bellezza di ciò. Se volessi ingenuamente scrivere funzioni che evitassero effetti collaterali che introducono una struttura dati che richiede gigabyte di memoria e modifica solo il valore di un megabyte di memoria, allora dovresti copiare l'intera cosa fottuta per evitare di toccare l'input e restituire un nuovo produzione. È in grado di copiare gigabyte per evitare effetti collaterali o causare effetti collaterali in tale scenario, rendendo necessario scegliere tra due scelte spiacevoli.
Con una struttura dati persistente, ti permette di scrivere una tale funzione ed evitare di fare una copia dell'intera struttura dati, richiedendo solo circa un megabyte di memoria extra per l'output se la tua funzione ha solo trasformato il valore di un megabyte di memoria.
Burden
Per quanto riguarda il peso, ce n'è uno immediato, almeno nel mio caso. Ho bisogno di quei costruttori di cui si parla o di "transitori" come li chiamo per essere in grado di esprimere efficacemente le trasformazioni a quella massiccia struttura di dati senza toccarla. 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 queste due righe di codice aggiuntive, la funzione ora è sicura per chiamare i thread con lo stesso elenco originale, non causa effetti collaterali, ecc. Inoltre rende davvero facile rendere questa operazione un utente annullabile azione dal momento che l'annullamento può solo memorizzare una copia poco costoso della vecchia lista.
Eccezione: sicurezza o ripristino degli errori
Non tutti potrebbero trarne beneficio quanto da strutture di dati persistenti in contesti come questi (ho trovato così utile per loro nei sistemi di annullamento e nell'editing non distruttivo che sono concetti centrali nel mio dominio VFX), ma una cosa applicabile a quasi tutti quelli da considerare è exception-safety o recovery error .
Se vuoi rendere la funzione di muting originale eccezionalmente sicura, allora ha bisogno della logica di rollback, per la quale l'implementazione più semplice richiede la copia dell'elenco intero :
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 eccezionalmente sicura è ancora più dispendiosa dal punto di vista computazionale e forse anche più difficile da scrivere correttamente rispetto alla versione immutabile che usa un "builder". E molti sviluppatori C ++ trascurano la sicurezza delle eccezioni e forse questo 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 test di scrittura che deliberatamente generano eccezioni per testare l'eccezione sicurezza), e questo lo rende così che devo essere in grado di eseguire il rollback di qualsiasi effetto collaterale che una funzione causa a metà nella funzione se qualcosa genera.
Quando vuoi essere protetto da eccezioni e recuperare dagli errori con garbo senza il crash e la masterizzazione delle applicazioni, devi annullare / annullare eventuali effetti collaterali che una funzione può causare in caso di errore / eccezione. E lì il costruttore può effettivamente risparmiare più tempo per il programmatore di quanto costi con il tempo di calcolo perché: ...
You don't have to worry about rolling back side effects in a function which doesn't cause any!
Quindi torna alla domanda fondamentale:
At what point do immutable classes become a burden?
Sono sempre un peso nei linguaggi che ruotano maggiormente attorno alla mutabilità rispetto all'immutabilità, motivo per cui penso che dovresti usarli laddove i benefici superino significativamente i costi. Ma a un livello abbastanza ampio per strutture di dati abbastanza grandi, credo che ci siano molti casi in cui è un compromesso degno.
Anche nella mia, 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 / bordi / poligoni di una mesh).