Alors à quel moment une classe devient-elle trop complexe pour être immuable?
À mon avis, cela ne vaut pas la peine de rendre les petites classes immuables dans des langues comme celle que vous montrez. J'utilise petit ici et pas complexe , car même si vous ajoutez dix champs à cette classe et qu'il y a vraiment des opérations fantaisistes sur eux, je doute que ça va prendre des kilo-octets et encore moins des mégaoctets et encore moins des gigaoctets, donc toute fonction utilisant des instances de votre La classe peut simplement faire une copie bon marché de l'objet entier pour éviter de modifier l'original si elle veut éviter de provoquer des effets secondaires externes.
Structures de données persistantes
Là où je trouve que l'utilisation personnelle de l'immuabilité est pour les grandes structures de données centrales qui regroupent un tas de données minuscules comme des instances de la classe que vous montrez, comme celle qui en stocke un million NamedThings
. En appartenant à une structure de données persistante qui est immuable et en se trouvant derrière une interface qui ne permet qu'un accès en lecture seule, les éléments qui appartiennent au conteneur deviennent immuables sans que la classe d'élément ( NamedThing
) n'ait à y faire face.
Copies bon marché
La structure de données persistante permet à certaines de ses régions d'être transformées et rendues uniques, en évitant les modifications de l'original sans avoir à copier la structure de données dans son intégralité. C'est ça la vraie beauté. Si vous vouliez écrire naïvement des fonctions qui évitent les effets secondaires qui entrent dans une structure de données qui prend des gigaoctets de mémoire et ne modifie que la valeur d'un mégaoctet de mémoire, alors vous devrez copier la totalité de la chose pour éviter de toucher l'entrée et renvoyer une nouvelle production. Il s'agit soit de copier des gigaoctets pour éviter les effets secondaires ou de provoquer des effets secondaires dans ce scénario, ce qui vous oblige à choisir entre deux choix désagréables.
Avec une structure de données persistante, il vous permet d'écrire une telle fonction et d'éviter de faire une copie de la structure de données entière, ne nécessitant environ un mégaoctet de mémoire supplémentaire pour la sortie que si votre fonction a seulement transformé la valeur de la mémoire d'un mégaoctet.
Fardeau
Quant au fardeau, il y en a un au moins dans mon cas. J'ai besoin de ces constructeurs dont les gens parlent ou de "transitoires" comme je les appelle pour pouvoir exprimer efficacement les transformations de cette structure de données massive sans y toucher. Code comme celui-ci:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... doit alors être écrit comme ceci:
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);
}
Mais en échange de ces deux lignes de code supplémentaires, la fonction est désormais sûre d'appeler à travers des threads avec la même liste d'origine, elle ne provoque aucun effet secondaire, etc. Cela rend également très facile de faire de cette opération une action utilisateur annulable puisque le annuler peut simplement stocker une copie peu profonde bon marché de l'ancienne liste.
Sécurité d'exception ou récupération d'erreur
Tout le monde ne pourrait pas bénéficier autant que moi des structures de données persistantes dans des contextes comme ceux-ci (j'ai trouvé tellement d'utilité pour eux dans les systèmes d'annulation et l'édition non destructive qui sont des concepts centraux dans mon domaine VFX), mais une chose applicable à peu près tout le monde à considérer est la sécurité d'exception ou la récupération d'erreur .
Si vous souhaitez sécuriser la fonction de mutation d'origine sans exception, il faut une logique de restauration, pour laquelle la mise en œuvre la plus simple nécessite de copier la liste entière :
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;
}
}
À ce stade, la version mutable sans exception est encore plus coûteuse en termes de calcul et sans doute encore plus difficile à écrire correctement que la version immuable utilisant un "générateur". Et beaucoup de développeurs C ++ négligent simplement la sécurité des exceptions et c'est peut-être bien pour leur domaine, mais dans mon cas, j'aime m'assurer que mon code fonctionne correctement même en cas d'exception (même en écrivant des tests qui lèvent délibérément des exceptions pour tester l'exception) sécurité), et cela fait que je dois pouvoir annuler tous les effets secondaires qu'une fonction provoque à mi-chemin de la fonction si quelque chose se déclenche.
Lorsque vous voulez être protégé contre les exceptions et récupérer des erreurs avec élégance sans que votre application ne se bloque et ne brûle, vous devez annuler / annuler les effets secondaires qu'une fonction peut provoquer en cas d'erreur / d'exception. Et là, le constructeur peut réellement économiser plus de temps programmeur qu'il n'en coûte avec du temps de calcul car: ...
Vous n'avez pas à vous soucier de faire reculer les effets secondaires dans une fonction qui n'en provoque pas!
Revenons donc à la question fondamentale:
À quel moment les classes immuables deviennent-elles un fardeau?
Ils sont toujours un fardeau dans les langages qui tournent plus autour de la mutabilité que de l'immuabilité, c'est pourquoi je pense que vous devriez les utiliser lorsque les avantages l'emportent largement sur les coûts. Mais à un niveau suffisamment large pour des structures de données suffisamment grandes, je pense qu'il existe de nombreux cas où c'est un compromis valable.
Toujours dans le mien, je n'ai que quelques types de données immuables, et ce sont toutes d'énormes structures de données destinées à stocker un nombre massif d'éléments (pixels d'une image / texture, entités et composants d'un ECS, et sommets / bords / polygones de un maillage).