Il semble que la sécurité des threads soit toujours / souvent mentionnée comme le principal avantage de l'utilisation de types immuables et notamment de 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 #). J'aimerais limiter les choses autant que possible.
Cependant, je ne suis pas sûr que l'ajout d'une dépendance à un nouveau package (Microsoft Immutable Collections) en vaille la peine. La performance n'est pas un gros problème non plus.
Donc, je suppose que ma question est de savoir si les collections immuables sont fortement recommandé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 de valeur (comme dans mon exemple) pourrait ou non être une exigence.
la source
ConcurrentModificationException
qui est généralement causé par le même thread qui mute la collection dans le même thread, dans le corps d'uneforeach
boucle sur la même collection.hashCode()
ou si leequals(Object)
résultat est modifié, des erreurs peuvent survenir lors de l'utilisationCollections
(par exemple,HashSet
l'objet a été stocké dans un "compartiment" et, après la mutation, il doit être remplacé par un autre).Réponses:
L'immuabilité simplifie la quantité d'informations que vous devez suivre mentalement lors de la lecture ultérieure du code . Pour les variables modifiables, et en particulier les membres de la classe modifiables, il est très difficile de savoir dans quel état elles se trouveront sur la ligne spécifique que vous lisez, sans exécuter le code avec un débogueur. Il est facile de raisonner sur des données immuables - ce sera toujours la même chose. Si vous voulez le changer, vous devez créer une nouvelle valeur.
Honnêtement, je préférerais 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 de la performance ou qu'un algorithme que vous avez n'a pas de sens pour l'immutabilité.
la source
val
, et seulement à de très rares occasions, je trouve que je dois changer quelque chose en unvar
. Un grand nombre des «variables» que je définis dans une langue donnée ne sont là que pour contenir une valeur qui stocke le résultat d'un calcul, et n'a pas besoin d'être mise à jour.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 est mieux exprimée.
Comment pourriez-vous savoir que dans le code suivant, la modification du nom entraînerait l'application à générer une exception quelque part plus tard?
Il est plus facile de s’assurer que l’objet n’apparaîtrait pas dans un état non valide.
Vous devez contrôler cela dans un constructeur, et seulement là. Par contre, si vous avez un ensemble de paramètres et de méthodes qui modifient l’objet, de tels contrôles peuvent devenir particulièrement difficiles, notamment 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 si les coordonnées GPS ne le sont pasnull
, mais il est invalide si l'adresse et les coordonnées GPS sont spécifiées. Pouvez-vous imaginer l'enfer pour valider cela si l'adresse et les coordonnées GPS ont un setter, ou les deux sont mutables?La concurrence.
À propos, dans votre cas, vous n’avez pas besoin de paquets tiers. .NET Framework comprend déjà une
ReadOnlyDictionary<TKey, TValue>
classe.la source
Il y a de nombreuses raisons d'utiliser l'immutabilité dans un seul thread. Par exemple
L'objet A contient l'objet B.
Le code externe interroge votre objet B et vous le renvoyez.
Maintenant, vous avez trois situations possibles:
Dans le troisième cas, le code utilisateur peut ne pas réaliser ce que vous avez fait et peut modifier l'objet, ce qui modifie les données internes de votre objet, sans aucun contrôle ni visibilité sur ce qui se passe.
la source
L’immuabilité peut également grandement simplifier la mise en œuvre des éboueurs. Du wiki de GHC :
la source
En développant ce que KChaloux a 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 modifiables, et nous devons écrire du code qui prend un instantané (
int x = p.x
) ou gère gracieusement de telles modifications.D'après mon expérience, la plupart du code se situe entre les deux, étant optimiste : il référence librement des 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 ça ne l'est plus. Oops.Alors, vraiment, retournez cette question: quelles sont mes raisons pour rendre ce mutable ?
Est-ce que vous écrivez du code défensif? L’immuabilité vous épargnera quelques copies. Est-ce que vous écrivez du code optimiste? L'immuabilité vous épargnera la folie de ce bug étrange et impossible.
la source
Un autre avantage de l'immuabilité est qu'il s'agit de la première étape pour arrondir ces objets immuables dans un pool. Ensuite, vous pouvez les gérer de manière à ne pas créer plusieurs objets qui représentent de manière conceptuelle et sémantique la même chose. Un bon exemple serait la chaîne Java.
Il est bien connu en linguistique que quelques mots apparaissent souvent, peuvent également apparaître dans un autre contexte. Ainsi, au lieu de créer plusieurs
String
objets, vous pouvez en utiliser un immuable. Mais vous devez alors garder un gestionnaire de pool pour prendre soin de ces objets immuables.Cela vous fera économiser beaucoup de mémoire. C'est aussi un article intéressant à lire: http://en.wikipedia.org/wiki/Zipf%27s_law
la source
Dans Java, C # et d'autres langages similaires, les champs de type classe peuvent être utilisés pour identifier des objets, ou pour encapsuler des valeurs ou des états dans ces objets, mais les langages ne font aucune distinction entre ces utilisations. Supposons qu'un objet de classe
George
possède un champ de typechar[] chars;
. Ce champ peut encapsuler une séquence de caractères de l'une des manières suivantes:Un tableau qui ne sera jamais modifié, ni exposé à un code susceptible de le modifier, mais pour lequel des références externes peuvent exister.
Un tableau pour lequel il n'existe aucune référence externe, mais que George peut modifier librement.
Un tableau appartenant à George, mais auquel il peut exister des vues extérieures qui représenteraient l'état actuel de George.
De plus, 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
actuellement encapsule la séquence de caractères [vent], et que George souhaitechars
encapsuler la séquence de caractères [baguette magique], il y a un certain nombre de choses que George pourrait faire:A. Construisez un nouveau tableau contenant les caractères [baguette] et changez
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 [baguette magique] et changez
chars
pour identifier ce tableau plutôt que l'ancien.C. Modifier le deuxième caractère du tableau identifié par
chars
una
.Dans le cas 1, (A) et (B) sont des moyens sûrs d’atteindre le résultat souhaité. Dans le cas où (2), (A) et (C) sont sûrs, mais (B) ne le serait pas [cela ne poserait pas de problèmes immédiats, mais puisque George supposerait qu'il est propriétaire du réseau, il supposerait que pourrait changer le tableau à volonté]. Dans le cas (3), les choix (A) et (B) briseraient les vues extérieures et 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 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 utilise le typeString
, qui encapsule une séquence de caractères immuable, tous les problèmes ci-dessus disparaissent. Tous les champs de typeString
encapsulent une séquence de caractères à l'aide d'un objet partageable qui ne changera jamais. Par conséquent, si un champ de typeString
encapsule "le vent", le seul moyen de le faire encapsuler "baguette magique" est de lui faire identifier un objet différent - celui qui contient "baguette magique". 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 mutable, il est nécessaire de distinguer les différents moyens par lesquels elle peut encapsuler une valeur. Personnellement, j’estime que Apps Hungarian aurait dû être utilisé à cet effet (je considérerais que les quatre utilisations dechar[]
sont des types distincts du point de vue sémantique, même si le système de types les considère identiques, ce qui correspond exactement au type de situation où Apps Hungarian brille). Pour éviter de telles ambiguïtés, le plus simple n’était pas de concevoir des types immuables qui encapsulent les valeurs d’une manière ou d’une autre.la source
Il y a quelques exemples intéressants ici, mais je voulais intervenir avec des exemples personnels où l'immutabilité a aidé énormément. Dans mon cas, j'ai commencé par concevoir une structure de données concurrente immuable, principalement dans l'espoir de pouvoir exécuter du code en toute confiance en parallèle avec des lectures et des écritures qui se chevauchent, sans avoir à se soucier des conditions de concurrence. Il y a eu une discussion que John Carmack a donnée m'a inspiré à le faire lorsqu'il a parlé d'une telle idée. C'est une structure assez basique et triviale à implémenter comme ceci:
Bien sûr, avec quelques cloches et des sifflets de plus, c'est comme être capable de supprimer des éléments en temps constant et de laisser des trous récupérables derrière et de laisser les blocs se dérégler 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 engagez de manière atomique 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 doivent être rendus uniques tout en faisant une copie superficielle et en comptant les autres.
Cependant, je ne trouve pas queutile pour le multithreading. Après tout, il y a toujours le problème conceptuel selon lequel, par exemple, un système physique applique la physique simultanément alors 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? Donc, je n'ai pas vraiment trouvé de solution simple et agréable à ce problème conceptuel de base, si ce n'est d'avoir des structures de données modifiables 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 les problèmes de threads. C'est quelque chose que John Carmack semble avoir probablement compris comment résoudre ses problèmes. au moins, il en parle comme s'il pouvait presque trouver 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 mettre en parallèle avec Immutables. J'aimerais pouvoir passer une journée à le cueillir dans la tête, car la plupart de mes efforts ont débuté avec ces idées qu'il a proposées.
Néanmoins, j'ai trouvé une valeur énorme de cette structure de données immuable dans d'autres domaines. Je l'utilise même maintenant pour stocker des images vraiment bizarres et rendant l'accès aléatoire nécessitant quelques instructions supplémentaires (décalage droit et binaire
and
avec couche de pointeur indirectionnel), mais je traiterai des avantages ci-dessous.Annuler le système
L'un des endroits les plus immédiats dont j'ai tiré profit était le système d'annulation. Le code de système d'annulation était l'une des choses les plus sujettes aux erreurs de ma région (secteur des effets visuels), et pas seulement dans les produits sur lesquels j'ai travaillé, mais aussi dans les produits concurrents (leurs systèmes d'annulation étaient également irréguliers) car il y avait tellement de solutions différentes. types de données à craindre d'annuler et de refaire correctement (système de propriétés, changements de données de maillage, changements de shader qui n'étaient pas basés sur des propriétés, comme l'échange entre eux, changements de hiérarchie de scènes comme changer le parent d'un enfant, changements d'image / texture, etc. etc. etc.).
Ainsi, la quantité de code d'annulation nécessaire é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:
Normalement, le code ci-dessus serait extrêmement inefficace lorsque vos données de scène couvrent des gigaoctets pour le copier intégralement. Mais cette structure de données ne copie que de manière superficielle les choses qui n'ont pas changé, et elle permet en fait de stocker, de manière peu coûteuse, une copie immuable de l'état complet de l'application. Je peux donc maintenant mettre en œuvre des systèmes d'annulation aussi facilement que le code ci-dessus et me concentrer sur l'utilisation de cette structure de données immuable pour rendre la copie des parties non modifiées de l'état de l'application moins chère, moins chère et moins chère. Depuis que j'ai commencé à utiliser cette structure de données, tous mes projets personnels ont des systèmes d'annulation utilisant uniquement ce modèle simple.
Maintenant, il y a encore des frais généraux ici. La dernière fois que j’ai mesuré que c’était environ 10 kilo-octets, il suffit de copier superficiellement l’état complet de l’application sans y apporter de modification (ceci est indépendant de la complexité de la scène car celle-ci est organisée dans une hiérarchie. Ainsi, rien ne change en dessous de la racine, est copié peu profond sans avoir à descendre dans les enfants). C’est loin de 0 octets, ce qui serait nécessaire pour un système d'annulation stockant uniquement des deltas. Toutefois, avec 10 kilo-octets de temps supplémentaire d'annulation par opération, il ne reste qu'un mégaoctet pour 100 opérations utilisateur. De plus, je pourrais encore potentiellement le réduire davantage si nécessaire.
Sécurité d'exception
La sécurité des exceptions avec une application complexe n’est pas une mince affaire. Cependant, lorsque l'état de votre application est immuable et que vous utilisez uniquement des objets transitoires pour tenter de valider des transactions de changement atomique, il est intrinsèquement protégé contre les exceptions, car si une partie du code est jetée, le composant transitoire est éliminé avant de donner une nouvelle copie immuable. . Cela banalise donc l’une des choses les plus difficiles que j’ai toujours trouvées à réussir dans une base de code C ++ complexe.
Trop de gens utilisent souvent simplement des ressources conformes à RAII en C ++ et pensent que cela suffit pour être à l'abri des exceptions. Ce n’est souvent pas le cas, puisqu’une fonction peut généralement causer des effets secondaires aux États situés au-delà de ceux situés localement. Dans ces cas, vous devez généralement commencer à traiter avec des gardes-objectifs et une logique de retour en arrière sophistiquée. Cette structure de données a été conçue pour que je n'ai souvent pas à m'en préoccuper, car les fonctions ne causent pas d'effets secondaires. Ils renvoient des copies immuables transformées de l'état de l'application au lieu de transformer l'état de l'application.
Montage non destructif
L’édition non destructive consiste essentiellement à superposer, empiler et relier des opérations sans toucher aux données de l’utilisateur original (il suffit de saisir des données et d’en sortir des données sans toucher à l’entrée). Il est généralement trivial de le mettre en œuvre avec une simple application d’image, telle que Photoshop, et cette structure de données ne bénéficiera peut-être pas beaucoup de cette opération, car de nombreuses opérations pourraient vouloir transformer chaque pixel de l’ensemble de l’image.
Cependant, avec l'édition de maillage non destructif, par exemple, de nombreuses opérations ne veulent souvent transformer qu'une partie du maillage. Une opération peut simplement vouloir déplacer des sommets ici. Un autre pourrait simplement vouloir subdiviser certains polygones à cet endroit. Ici, la structure de données immuable aide beaucoup à éviter la nécessité de faire une copie complète de tout le maillage pour renvoyer une nouvelle version du maillage avec une petite partie de celle-ci modifiée.
Minimiser les effets secondaires
Avec ces structures en main, il est également facile d’écrire des fonctions qui minimisent les effets secondaires sans encourir d’énormes pertes 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 derniers temps sans que cela n'entraîne d'effets secondaires, même lorsque cela semble un peu inutile.
Par exemple, la tentation de transformer un groupe de positions consiste généralement à accepter une matrice et une liste d'objets et à les transformer de manière mutable. Ces jours-ci, je me retrouve à renvoyer une nouvelle liste d'objets.
Lorsque votre système contient davantage de fonctions comme celle-ci ne causant pas d'effets secondaires, il est nettement plus facile de raisonner au sujet de son exactitude, ainsi que de le tester.
Les avantages des copies bon marché
Donc de toute façon, ce sont les domaines dans lesquels j'ai trouvé le plus d'utilisation de structures de données immuables (ou de structures de données persistantes). Au départ, j’ai eu un peu trop de zèle et j’ai créé un arbre immuable, une liste chaînée immuable et une table de hachage immuable, mais au fil du temps, j’ai rarement trouvé autant d’utilisation pour ceux-ci. Dans le diagramme ci-dessus, j'ai principalement trouvé l'utilisation la plus fréquente du conteneur en forme de tableau immuable et volumineux.
Il me reste également beaucoup de code travaillant avec des mutables (la pratique le trouve moins nécessaire pour le code de bas niveau), mais l’état principal de l’application est une hiérarchie immuable, descendant d’une scène immuable à des composants immuables qui la composent. Certains des composants les moins chers sont toujours intégralement copiés, mais les plus chers, tels que les maillages et les images, utilisent la structure immuable pour autoriser les copies partielles peu coûteuses des seules pièces à transformer.
la source
Il y a déjà beaucoup de bonnes réponses. Ceci est juste une information supplémentaire quelque peu spécifique à .NET. Je cherchais dans 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:
la source