Il semble que la sécurité des threads soit toujours/souvent mentionnée comme le principal avantage de l'utilisation de types immuables et en particulier des collections.
J'ai une situation où je voudrais m'assurer qu'une méthode ne modifiera pas un dictionnaire de chaînes (qui sont immuables en C #). Je voudrais limiter autant que possible les choses.
Cependant, je ne sais pas si l'ajout d'une dépendance à un nouveau package (Microsoft Immutable Collections) en vaut la peine. La performance n'est pas non plus un gros problème.
Donc, je suppose que ma question est de savoir si les collections immuables sont fortement conseillées lorsqu'il n'y a pas d'exigences de performances strictes et qu'il n'y a pas de problèmes de sécurité des threads? Considérez que la sémantique des valeurs (comme dans mon exemple) pourrait ou non être une exigence également.
l'immuabilité simplifie la quantité d'informations dont vous avez besoin pour effectuer un suivi mental lors de la lecture ultérieure du code. Pour les variables mutables, et en particulier les membres de classe mutables, il est très difficile de savoir dans quel état ils se trouveront sur la ligne spécifique sur laquelle vous lisez, sans parcourir le code avec un débogueur. Les données immuables sont faciles à comprendre - elles seront toujours les mêmes. Si vous souhaitez le modifier, vous devez créer une nouvelle valeur.
Je préférerais honnêtement rendre les choses immuables par défaut, puis les changer en mutables là où il est prouvé qu'elles doivent l'être, que cela signifie que vous avez besoin des performances ou qu'un algorithme que vous avez n'a pas de sens pour l'immuabilité.
Votre code doit exprimer votre intention. Si vous ne voulez pas qu'un objet soit modifié une fois créé, rendez-le impossible.
L'immuabilité présente plusieurs avantages:
L'intention de l'auteur original s'exprime mieux.
Comment pourriez-vous savoir que dans le code suivant, la modification du nom entraînerait la génération d'une exception quelque part par la suite?
public class Product
{
public string Name { get; set; }
...
}
Il est plus facile de s'assurer que l'objet n'apparaîtra pas dans un état invalide.
Vous devez contrôler cela dans un constructeur, et seulement là. D'un autre côté, si vous disposez d'un ensemble de paramètres et de méthodes qui modifient l'objet, de tels contrôles peuvent devenir particulièrement difficiles, en particulier lorsque, par exemple, deux champs doivent changer en même temps pour que l'objet soit valide.
Par exemple, un objet est valide si l'adresse n'est pas null
ou Les coordonnées GPS ne sont pas null
, mais elles ne sont pas valides si l'adresse et les coordonnées GPS sont spécifiés. Pouvez-vous imaginer l'enfer pour valider cela si l'adresse et les coordonnées GPS ont un setter, ou si les deux sont mutables?
Concurrence.
Soit dit en passant, dans votre cas, vous n'avez pas besoin de packages tiers. .NET Framework comprend déjà un ReadOnlyDictionary<TKey, TValue>
classe.
Il existe de nombreuses raisons d'utiliser un seul thread pour l'immuabilité. Par exemple
L'objet A contient l'objet B.
Le code externe interroge votre objet B et vous le renvoyez.
Vous avez maintenant trois situations possibles:
Dans le troisième cas, le code utilisateur peut ne pas réaliser ce que vous avez fait et peut apporter des modifications à l'objet et, ce faisant, modifier les données internes de votre objet sans que vous ayez le contrôle ou la visibilité de ce qui se passe.
L'immuabilité peut également simplifier considérablement la mise en œuvre des collecteurs de déchets. De wiki du GHC :
[...] L'immuabilité des données nous oblige à produire beaucoup de données temporaires mais elle permet également de collecter ces déchets rapidement. L'astuce est que les données immuables ne pointent JAMAIS vers des valeurs plus jeunes. En effet, les valeurs plus jeunes n'existent pas encore au moment où une ancienne valeur est créée, donc elle ne peut pas être pointée de zéro. Et comme les valeurs ne sont jamais modifiées, elles ne peuvent pas non plus être signalées plus tard. Il s'agit de la propriété clé des données immuables.
Cela simplifie considérablement la collecte des ordures (GC). À tout moment, nous pouvons analyser les dernières valeurs créées et libérer celles qui ne sont pas pointées du même ensemble (bien sûr, les vraies racines de la hiérarchie des valeurs en direct sont en direct dans la pile). [...] Il a donc un comportement contre-intuitif: le plus grand pourcentage de vos valeurs sont des ordures - plus vite cela fonctionne. [...]
Développer sur quoi KChaloux très bien résumé ...
Idéalement, vous avez deux types de champs, et donc deux types de code les utilisant. Les deux champs sont immuables et le code n'a pas à prendre en compte la mutabilité; ou les champs sont mutables, et nous devons écrire du code qui prend soit un instantané (int x = p.x
) ou gère gracieusement ces modifications.
D'après mon expérience, la plupart du code se situe entre les deux, étant optimiste code: il référence librement les données mutables, en supposant que le premier appel à p.x
aura le même résultat que le deuxième appel. Et la plupart du temps, c'est vrai, sauf quand il s'avère que ce n'est plus le cas. Oops.
Donc, vraiment, retournez cette question: Quelles sont mes raisons pour rendre ce mutable ?
Écrivez-vous du code défensif? L'immutabilité vous fera économiser de la copie. Écrivez-vous du code optimiste? L'immutabilité vous épargnera la folie de ce bug étrange et impossible.
Un autre avantage de l'immuabilité est qu'il s'agit de la première étape pour rassembler ces objets immuables dans une piscine. Vous pouvez ensuite les gérer afin de ne pas créer plusieurs objets qui représentent conceptuellement et sémantiquement la même chose. Un bon exemple serait la chaîne de Java.
C'est un phénomène bien connu en linguistique que quelques mots apparaissent beaucoup, pourraient aussi apparaître dans un autre contexte. Ainsi, au lieu de créer plusieurs objets String
, vous pouvez en utiliser un immuable. Mais alors vous devez garder un gestionnaire de pool pour prendre soin de ces objets immuables.
Cela vous fera économiser beaucoup de mémoire. Il s'agit également d'un article intéressant à lire: http://en.wikipedia.org/wiki/Zipf%27s_law
Il y a quelques bons exemples ici, mais je voulais sauter avec quelques exemples personnels où l'immuabilité a aidé une tonne. Dans mon cas, j'ai commencé à concevoir une structure de données simultanée immuable principalement dans l'espoir de pouvoir exécuter en toute confiance du code en parallèle avec des lectures et des écritures qui se chevauchent et sans avoir à se soucier des conditions de concurrence. Il y a eu un discours que John Carmack m'a donné ce genre d'inspiration pour le faire quand il a parlé d'une telle idée. C'est une structure assez basique et assez triviale à implémenter comme ceci:
Bien sûr, avec quelques cloches et sifflets supplémentaires, comme pouvoir retirer des éléments en temps constant et laisser des trous récupérables derrière et faire bloquer les blocs s'ils deviennent vides et potentiellement libérés pour une instance immuable donnée. Mais fondamentalement, pour modifier la structure, vous modifiez une version "transitoire" et vous validez atomiquement les modifications que vous y avez apportées pour obtenir une nouvelle copie immuable qui ne touche pas l'ancienne, la nouvelle version ne créant que de nouvelles copies des blocs qui doivent être rendus uniques lors de la copie superficielle et du comptage des références des autres.
Cependant, je ne l'ai pas trouvé ça utile à des fins de multithreading. Après tout, il y a toujours le problème conceptuel où, disons, un système de physique applique la physique simultanément pendant qu'un joueur essaie de déplacer des éléments dans un monde. Avec quelle copie immuable des données transformées allez-vous, celle que le joueur a transformée ou celle que le système physique a transformée? Je n'ai donc pas vraiment trouvé de solution simple et agréable à ce problème conceptuel de base, sauf pour avoir des structures de données mutables qui se verrouillent de manière plus intelligente et découragent les lectures et les écritures qui se chevauchent dans les mêmes sections du tampon pour éviter de bloquer les threads. C'est quelque chose que John Carmack semble avoir peut-être trouvé comment résoudre dans ses jeux; au moins, il en parle comme s'il pouvait presque voir une solution sans ouvrir une voiture de vers. Je ne suis pas allé aussi loin que lui à cet égard. Tout ce que je peux voir, ce sont des questions de conception sans fin si j'essayais de tout paralléliser autour des immuables. J'aimerais pouvoir passer une journée à fouiller dans son cerveau, car la plupart de mes efforts ont commencé avec ces idées qu'il a rejetées.
Néanmoins, j'ai trouvé énorme la valeur de cette structure de données immuable dans d'autres domaines. Je l'utilise même maintenant pour stocker des images, ce qui est vraiment bizarre et rend l'accès aléatoire nécessite plus d'instructions (décalage à droite et un bitwise and
avec une couche d'indirection de pointeur), mais je couvrirai les avantages au dessous de.
Annuler le système
L'un des endroits les plus immédiats que j'ai trouvé pour en bénéficier était le système d'annulation. Le code système d'annulation était l'une des choses les plus sujettes aux erreurs dans mon domaine (industrie des effets visuels), et pas seulement dans les produits sur lesquels je travaillais, mais dans les produits concurrents (leurs systèmes d'annulation étaient également feuilletés) car il y avait tellement de différents types de données dont il faut se soucier pour annuler et refaire correctement (système de propriétés, modifications des données de maillage, modifications des nuanceurs qui n'étaient pas basées sur les propriétés comme les échanges les uns avec les autres, modifications de la hiérarchie des scènes comme le changement du parent d'un enfant, changements d'image/de texture, etc. etc. etc.).
Ainsi, la quantité de code d'annulation requise était énorme, rivalisant souvent avec la quantité de code implémentant le système pour lequel le système d'annulation devait enregistrer les changements d'état. En m'appuyant sur cette structure de données, j'ai pu réduire le système d'annulation à ceci:
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
Normalement, le code ci-dessus serait extrêmement inefficace lorsque vos données de scène s'étendent sur des gigaoctets pour les copier en entier. Mais cette structure de données ne copie que superficiellement des choses qui n'ont pas été modifiées, et elle a en fait permis de stocker suffisamment bon marché une copie immuable de l'ensemble de l'état de l'application. Alors maintenant, je peux implémenter des systèmes d'annulation aussi facilement que le code ci-dessus et me concentrer uniquement sur l'utilisation de cette structure de données immuable pour rendre la copie de parties inchangées de l'état de l'application moins cher et moins cher et moins cher. Depuis que j'ai commencé à utiliser cette structure de données, tous mes projets personnels ont des systèmes d'annulation simplement en utilisant ce modèle simple.
Maintenant, il y a encore des frais généraux ici. La dernière fois que j'ai mesuré, il s'agissait d'environ 10 kilo-octets juste pour copier superficiellement tout l'état de l'application sans y apporter de modifications (cela est indépendant de la complexité de la scène car la scène est organisée dans une hiérarchie, donc si rien en dessous de la racine ne change, seule la racine est peu profonde copiée sans avoir à descendre dans les enfants). C'est loin de 0 octet, ce qui serait nécessaire pour un système d'annulation qui ne stocke que des deltas. Mais à 10 kilo-octets de surcharge par opération, ce n'est toujours qu'un mégaoctet pour 100 opérations utilisateur. De plus, je pourrais potentiellement encore réduire cela à l'avenir si nécessaire.
Sécurité d'exception
La sécurité d'exception avec une application complexe n'est pas une mince affaire. Cependant, lorsque l'état de votre application est immuable et que vous n'utilisez que des objets transitoires pour essayer de valider des transactions de modification atomique, il est intrinsèquement protégé contre les exceptions car si une partie du code est lancée, le transitoire est jeté avant de donner une nouvelle copie immuable . Donc, cela banalise l'une des choses les plus difficiles que j'ai toujours trouvées pour réussir dans une base de code C++ complexe.
Trop de gens utilisent souvent des ressources conformes à RAII en C++ et pensent que cela suffit pour être à l'abri des exceptions. Souvent, ce n'est pas le cas, car une fonction peut généralement provoquer des effets secondaires sur des états au-delà de ceux locaux à sa portée. Dans ces cas, vous devez généralement commencer à gérer les protecteurs de portée et la logique de restauration sophistiquée. Cette structure de données l'a fait, donc je n'ai souvent pas besoin de m'embêter avec ça car les fonctions ne provoquent pas d'effets secondaires. Ils retournent des copies immuables transformées de l'état de l'application au lieu de transformer l'état de l'application.
Édition non destructive
L'édition non destructive consiste essentiellement à superposer/empiler/connecter des opérations ensemble sans toucher aux données de l'utilisateur d'origine (juste des données d'entrée et des données de sortie sans toucher à l'entrée). Il est généralement trivial de l'implémenter avec une application d'image simple comme Photoshop et peut ne pas bénéficier autant de cette structure de données, car de nombreuses opérations peuvent simplement vouloir transformer chaque pixel de l'image entière.
Cependant, avec l'édition non destructive du maillage, par exemple, de nombreuses opérations ne souhaitent souvent transformer qu'une partie du maillage. Une opération peut simplement vouloir déplacer certains sommets ici. Un autre pourrait simplement vouloir y subdiviser certains polygones. Ici, la structure de données immuable aide une tonne à éviter d'avoir à faire une copie complète du maillage entier juste pour renvoyer une nouvelle version du maillage avec une petite partie modifiée.
Réduction des effets secondaires
Avec ces structures en main, il facilite également l'écriture de fonctions qui minimisent les effets secondaires sans encourir une énorme pénalité en termes de performances. Je me suis retrouvé à écrire de plus en plus de fonctions qui ne font que restituer des structures de données immuables par valeur ces jours-ci sans encourir d'effets secondaires, même lorsque cela semble un peu inutile.
Par exemple, la tentation de transformer un ensemble de positions peut être d’accepter une matrice et une liste d’objets et de les transformer de manière modifiable. Ces jours-ci, je me retrouve à renvoyer une nouvelle liste d'objets.
Lorsque vous avez plus de fonctions comme celle-ci dans votre système qui ne provoquent aucun effet secondaire, il est certainement plus facile de raisonner sur son exactitude ainsi que de tester son exactitude.
Les avantages des copies bon marché
Donc, de toute façon, ce sont les domaines où j'ai trouvé le plus d'utilisation des structures de données immuables (ou des structures de données persistantes). J'ai également été un peu trop zélé au départ et j'ai créé un arbre immuable et une liste liée immuable et une table de hachage immuable, mais au fil du temps, j'ai rarement trouvé autant d'utilité pour ceux-ci. J'ai principalement trouvé la plus grande utilisation du conteneur de type tableau immuable en morceaux dans le diagramme ci-dessus.
J'ai également encore beaucoup de code fonctionnant avec des mutables (le trouve une nécessité pratique au moins pour le code de bas niveau), mais l'état principal de l'application est une hiérarchie immuable, descendant d'une scène immuable aux composants immuables à l'intérieur. Certains des composants les moins chers sont toujours copiés dans leur intégralité, mais les plus chers, comme les maillages et les images, utilisent la structure immuable pour permettre ces copies partielles à bas prix des seules pièces qui devaient être transformées.
En Java, C # et autres langages similaires, les champs de type classe peuvent être utilisés soit pour identifier des objets, soit pour encapsuler des valeurs ou des états dans ces objets, mais les langages ne font aucune distinction entre de tels usages. Supposons qu'un objet de classe George
possède un champ de type char[] chars;
. Ce champ peut encapsuler une séquence de caractères dans:
Un tableau qui ne sera jamais modifié, ni exposé à aucun code qui pourrait le modifier, mais auquel des références externes peuvent exister.
Un tableau auquel aucune référence externe n'existe, mais que George peut modifier librement.
Un tableau qui appartient à George, mais auquel il peut exister des vues extérieures qui devraient représenter l'état actuel de George.
En outre, la variable peut, au lieu d'encapsuler une séquence de caractères, encapsuler une vue en direct dans une séquence de caractères appartenant à un autre objet
Si chars
encapsule actuellement la séquence de caractères [w i n d], et que George souhaite que chars
encapsule la séquence de caractères [w a n d], George pourrait faire plusieurs choses:
A. Construisez un nouveau tableau contenant les caractères [w a n d] et modifiez chars
pour identifier ce tableau plutôt que l'ancien.
B. Identifiez en quelque sorte un tableau de caractères préexistant qui contiendra toujours les caractères [w a n d] et modifiez chars
pour identifier ce tableau plutôt que l'ancien.
C. Modifiez le deuxième caractère du tableau identifié par chars
en a
.
Dans le cas 1, (A) et (B) sont des moyens sûrs d'obtenir le résultat souhaité. Dans le cas (2), (A) et (C) sont sûrs, mais (B) ne serait pas [cela ne causerait pas de problèmes immédiats, mais puisque George supposerait qu'il est propriétaire du tableau, il supposerait qu'il pourrait changer le tableau à volonté]. Dans le cas (3), les choix (A) et (B) briseraient toutes les vues extérieures, et donc seul le choix (C) est correct. Ainsi, savoir comment modifier la séquence de caractères encapsulée par le champ nécessite de savoir de quel type de champ sémantique il s'agit.
Si au lieu d'utiliser un champ de type char[]
, qui encapsule une séquence de caractères potentiellement mutable, le code a utilisé le type String
, qui encapsule une séquence de caractères immuable, tous les problèmes ci-dessus disparaissent. Tous les champs de type String
encapsulent une séquence de caractères à l'aide d'un objet partageable qui ne changera jamais. Par conséquent, si un champ de type String
encapsule "vent", la seule façon de le faire encapsuler "baguette" est de lui faire identifier un objet différent - celui qui contient "baguette". Dans les cas où le code contient la seule référence à l'objet, la mutation de l'objet peut être plus efficace que la création d'une nouvelle, mais chaque fois qu'une classe est modifiable, il est nécessaire de distinguer les différentes manières par lesquelles elle peut encapsuler la valeur. Personnellement, je pense que les applications hongroises auraient dû être utilisées pour cela (je considérerais les quatre utilisations de char[]
pour être des types sémantiquement distincts, même si le système de types les considère identiques - exactement le genre de situation dans laquelle Apps Hongrois brille), mais comme ce n'était pas le moyen le plus simple d'éviter de telles ambiguïtés, c'est de concevoir des types immuables qui ne encapsuler les valeurs dans un sens.
Il y a déjà beaucoup de bonnes réponses. Il s'agit simplement d'une information supplémentaire quelque peu spécifique à .NET. Je fouillais d'anciens articles de blog .NET et j'ai trouvé un bon résumé des avantages du point de vue des développeurs de Microsoft Immutable Collections:
Sémantique des instantanés, vous permettant de partager vos collections d'une manière sur laquelle le récepteur peut compter sans jamais changer.
Sécurité implicite des threads dans les applications multithreads (aucun verrou requis pour accéder aux collections).
Chaque fois que vous avez un membre de classe qui accepte ou renvoie un type de collection et que vous souhaitez inclure une sémantique en lecture seule dans le contrat.
Programmation fonctionnelle conviviale.
Autorisez la modification d'une collection pendant l'énumération, tout en vous assurant que la collection d'origine ne change pas.
Ils implémentent les mêmes interfaces IReadOnly * que votre code traite déjà, donc la migration est facile.
Si quelqu'un vous remet une ReadOnlyCollection, une IReadOnlyList ou un IEnumerable, la seule garantie est que vous ne pouvez pas modifier les données - rien ne garantit que la personne qui vous a remis la collection ne la modifiera pas. Pourtant, vous avez souvent besoin d’être certain que cela ne changera pas. Ces types n'offrent pas d'événements pour vous avertir lorsque leur contenu change, et s'ils changent, cela peut-il se produire sur un fil différent, peut-être pendant que vous énumérez son contenu? Un tel comportement entraînerait une corruption des données et/ou des exceptions aléatoires dans votre application.