Pourquoi les structures mutables sont-elles «mauvaises»?

Suite aux discussions ici sur SO, j’ai déjà lu plusieurs fois la remarque que les structures mutables sont «mauvaises» (comme dans la réponse à cette question ).

Quel est le problème réel de la mutabilité et des structures en C #?

Les structures sont des types de valeur, ce qui signifie qu’elles sont copiées lorsqu’elles sont transmises.

Donc, si vous modifiez une copie, vous ne modifiez que cette copie, pas l’original et pas d’autres copies qui pourraient se trouver.

Si votre structure est immuable, toutes les copies automatiques résultant de la transmission par valeur seront les mêmes.

Si vous voulez le changer, vous devez le faire consciemment en créant une nouvelle instance de la structure avec les données modifiées. (pas une copie)

Où commencer ;-p

Le blog d’Eric Lippert est toujours bon pour une citation:

C’est une autre raison pour laquelle les types de valeurs mutables sont mauvais. Essayez de toujours rendre les types de valeur immuables.

Tout d’abord, vous avez tendance à perdre des modifications assez facilement … par exemple, en retirant des éléments d’une liste:

 Foo foo = list[0]; foo.Name = "abc"; 

qu’est-ce que ça a changé? Rien d’utile …

Le même avec des propriétés:

 myObj.SomeProperty.Size = 22; // the comstackr spots this one 

vous obliger à faire:

 Bar bar = myObj.SomeProperty; bar.Size = 22; myObj.SomeProperty = bar; 

de manière moins critique, il y a un problème de taille; les objects mutables ont tendance à avoir plusieurs propriétés; Cependant, si vous avez une structure avec deux int , une ssortingng , un DateTime et un bool , vous pouvez très rapidement graver beaucoup de mémoire. Avec une classe, plusieurs appelants peuvent partager une référence à la même instance (les références sont petites).

Je ne dirais pas mal, mais la mutabilité est souvent un signe de surmenage de la part du programmeur pour fournir un maximum de fonctionnalités. En réalité, cela n’est souvent pas nécessaire et cela rend l’interface plus petite, plus facile à utiliser et plus difficile à utiliser (= plus robuste).

Un exemple en est la lecture / écriture et les conflits écriture / écriture dans les conditions de course. Celles-ci ne peuvent tout simplement pas se produire dans des structures immuables, car une écriture n’est pas une opération valide.

En outre, je prétends que la mutabilité n’est presque jamais nécessaire , le programmeur pense que cela pourrait être dans le futur. Par exemple, changer une date n’a pas de sens. Au contraire, créez une nouvelle date basée sur l’ancienne. C’est une opération peu coûteuse, la performance n’est donc pas une considération.

Les structures modulables ne sont pas mauvaises.

Ils sont absolument nécessaires dans des conditions de haute performance. Par exemple, lorsque les lignes de cache et / ou la récupération de place deviennent un goulot d’étranglement.

Je n’appellerais pas l’utilisation d’une structure immuable dans ces cas d’utilisation parfaitement valables “mal”.

Je peux voir que la syntaxe de C # ne permet pas de distinguer l’access d’un membre d’un type valeur ou d’un type de référence. Je suis donc tout à fait pour préférer les structures immuables qui imposent l’immuabilité sur les structures mutables.

Cependant, au lieu de simplement étiqueter des structures immuables comme étant «mauvaises», je conseillerais d’emarmser le langage et de préconiser des règles de base plus utiles et constructives.

Par exemple: “structs sont des types de valeur, qui sont copiés par défaut. Vous avez besoin d’une référence si vous ne voulez pas les copier” ou “essayez de travailler avec readonly structs en premier” .

Les structures avec des champs ou propriétés mutables publiques ne sont pas mauvaises.

Les méthodes structurelles (différentes des méthodes de détermination des propriétés) qui modifient “ceci” sont quelque peu mauvaises, uniquement parce que .net ne permet pas de les distinguer des méthodes qui ne le font pas. Les méthodes de structure qui ne mutent pas “this” devraient pouvoir être appelées même sur des structures en lecture seule sans nécessiter de copie défensive. Les méthodes qui mutent “this” ne devraient pas être invocables du tout sur les structures en lecture seule. Etant donné que .net ne veut pas interdire aux méthodes struct qui ne modifient pas “this” d’être invoquées sur des structures en lecture seule, mais ne veut pas autoriser la mutation des structures en lecture seule, il copie de manière défensive les structures en lecture. seuls les contextes, sans doute le pire des deux mondes.

Malgré les problèmes liés au traitement des méthodes d’auto-mutation dans des contextes en lecture seule, les structures mutables offrent souvent une sémantique bien supérieure aux types de classe mutables. Considérez les trois signatures de méthode suivantes:

 struct PointyStruct {public int x, y, z;};
 classe PointyClass {public int x, y, z;};

 annuler la méthode 1 (foo PointyStruct);
 void Method2 (réf Foo PointyStruct);
 annuler la méthode 3 (pointyClass foo);

Pour chaque méthode, répondez aux questions suivantes:

  1. En supposant que la méthode n’utilise aucun code “dangereux”, pourrait-il modifier foo?
  2. Si aucune référence externe à ‘foo’ n’existe avant l’appel de la méthode, une référence externe pourrait-elle exister après?

Réponses:

Question 1:
Method1() : non (intention claire)
Method2() : oui (intention claire)
Method3() : oui (intention incertaine)
Question 2:
Method1() : non
Method2() : non (sauf si dangereux)
Method3() : oui

Method1 ne peut pas modifier foo et ne reçoit jamais de référence. Method2 obtient une référence de courte durée à foo, qu’il peut utiliser pour modifier les champs de foo autant de fois que nécessaire, dans n’importe quel ordre, jusqu’à ce qu’il revienne, mais il ne peut pas conserver cette référence. Avant que Method2 ne revienne, à moins d’utiliser un code non sécurisé, toutes les copies de sa référence “foo” auront disparu. Method3, à la différence de Method2, obtient une référence de façon prometteuse à foo, et rien ne dit quoi en faire. Cela ne changera peut-être pas du tout, il pourrait changer foo, puis retourner, ou donner une référence à foo à un autre thread qui pourrait le muter de manière arbitraire à un moment futur arbitraire. La seule façon de limiter ce que Method3 pourrait faire à un object de classe mutable transmis serait d’encapsuler l’object mutable dans un wrapper en lecture seule, ce qui est laid et encombrant.

Les tableaux de structures offrent une merveilleuse sémantique. Étant donné RectArray [500] de type Rectangle, il est clair et évident de copier par exemple l’élément 123 vers l’élément 456, puis de définir ultérieurement la largeur de l’élément 123 à 555, sans perturber l’élément 456. “RectArray [432] = RectArray [321 ]; …; RectArray [123] .Width = 555; “. Savoir que Rectangle est une structure avec un champ entier appelé Width indiquera tout ce que vous devez savoir sur les instructions ci-dessus.

Maintenant, supposons que RectClass était une classe avec les mêmes champs que Rectangle et que l’on voulait faire les mêmes opérations sur un RectClassArray [500] de type RectClass. Le tableau est peut-être supposé contenir 500 références immuables pré-initialisées à des objects RectClass mutables. dans ce cas, le code approprié serait quelque chose comme “RectClassArray [321] .SetBounds (RectClassArray [456]); …; RectClassArray [321] .X = 555;”. Peut-être que le tableau est supposé contenir des instances qui ne vont pas changer, donc le code approprié serait plus proche de “RectClassArray [321] = RectClassArray [456]; …; RectClassArray [321] = New RectClass (RectClassArray [321] ]); RectClassArray [321] .X = 555; ” Pour savoir ce que l’on est censé faire, il faudrait en savoir plus sur RectClass (par exemple, prend-il en charge un constructeur de copie, une méthode de copie depuis, etc.) et l’utilisation prévue du tableau. Nulle part aussi propre que d’utiliser une structure.

Pour être sûr, il n’y a malheureusement pas de moyen intéressant pour une classe de conteneur autre qu’un tableau d’offrir la sémantique propre d’un tableau struct. Le mieux que l’on puisse faire, si l’on voulait qu’une collection soit indexée avec, par exemple, une chaîne, serait probablement d’offrir une méthode générique “ActOnItem” qui accepterait une chaîne pour l’index, un paramètre générique et un délégué qui serait transmis par référence à la fois le paramètre générique et l’élément de collection. Cela autoriserait presque la même sémantique que les tableaux struct, mais à moins que les personnes vb.net et C # puissent être poursuivies pour offrir une syntaxe agréable, le code sera très maladroit même s’il est raisonnablement performant (le passage d’un paramètre générique autoriser l’utilisation d’un délégué statique et éviter de créer des instances de classe temporaires).

Personnellement, je suis énervé par la haine Eric Lippert et al. crachement concernant les types de valeurs mutables. Ils offrent une sémantique beaucoup plus propre que les types de référence prometteurs utilisés partout. Malgré certaines limitations de la prise en charge des types de valeur par .net, il existe de nombreux cas où les types de valeurs mutables sont plus adaptés que tout autre type d’entité.

Les types de valeur représentent essentiellement des concepts immuables. Fx, cela n’a aucun sens d’avoir une valeur mathématique telle qu’un nombre entier, un vecteur, etc. et de pouvoir ensuite la modifier. Ce serait comme redéfinir le sens d’une valeur. Au lieu de changer un type de valeur, il est plus judicieux d’atsortingbuer une autre valeur unique. Pensez au fait que les types de valeur sont comparés en comparant toutes les valeurs de ses propriétés. Le fait est que si les propriétés sont les mêmes, c’est la même représentation universelle de cette valeur.

Comme Konrad le mentionne, cela n’a aucun sens de changer une date non plus, car la valeur représente ce point unique dans le temps et non une instance d’un object temporel dépendant de l’état ou du contexte.

J’espère que cela a du sens pour vous. Il s’agit plus du concept que vous essayez de capturer avec des types de valeur que des détails pratiques, pour sûr.

Il existe deux autres cas en coin qui pourraient conduire à un comportement imprévisible du sharepoint vue des programmeurs. Voici quelques-uns d’entre eux.

  1. Types de valeurs immuables et champs en lecture seule
 // Simple mutable structure. // Method IncrementI mutates current state. struct Mutable { public Mutable(int i) : this() { I = i; } public void IncrementI() { I++; } public int I {get; private set;} } // Simple class that contains Mutable structure // as readonly field class SomeClass { public readonly Mutable mutable = new Mutable(5); } // Simple class that contains Mutable structure // as ordinary (non-readonly) field class AnotherClass { public Mutable mutable = new Mutable(5); } class Program { void Main() { // Case 1. Mutable readonly field var someClass = new SomeClass(); someClass.mutable.IncrementI(); // still 5, not 6, because SomeClass.mutable field is readonly // and comstackr creates temporary copy every time when you trying to // access this field Console.WriteLine(someClass.mutable.I); // Case 2. Mutable ordinary field var anotherClass = new AnotherClass(); anotherClass.mutable.IncrementI(); //Prints 6, because AnotherClass.mutable field is not readonly Console.WriteLine(anotherClass.mutable.I); } } 

  1. Types de valeurs et tableau mutables

Supposons que nous ayons un tableau de notre structure Mutable et que nous appelons la méthode IncrementI pour le premier élément de ce tableau. Quel comportement attendez-vous de cet appel? Doit-il changer la valeur du tableau ou seulement une copie?

 Mutable[] arrayOfMutables = new Mutable[1]; arrayOfMutables[0] = new Mutable(5); // Now we actually accessing reference to the first element // without making any additional copy arrayOfMutables[0].IncrementI(); //Prints 6!! Console.WriteLine(arrayOfMutables[0].I); // Every array implements IList interface IList listOfMutables = arrayOfMutables; // But accessing values through this interface lead // to different behavior: IList indexer returns a copy // instead of an managed reference listOfMutables[0].IncrementI(); // Should change I to 7 // Nope! we still have 6, because previous line of code // mutate a copy instead of a list value Console.WriteLine(listOfMutables[0].I); 

Donc, les structures mutables ne sont pas mauvaises tant que vous et le rest de l’équipe comprenez clairement ce que vous faites. Mais il y a trop de cas particuliers où le comportement du programme serait différent de celui attendu, ce qui pourrait conduire à des erreurs subtiles difficiles à produire et difficiles à comprendre.

Si vous avez déjà programmé dans un langage comme le C / C ++, les structures peuvent être utilisées comme mutables. Il suffit de les passer avec ref, et il n’y a rien qui puisse mal tourner. Le seul problème que je trouve sont les ressortingctions du compilateur C # et que, dans certains cas, je ne peux pas forcer la chose stupide à utiliser une référence à la structure, au lieu d’une copie (comme quand une structure fait partie d’une classe C #) ).

Donc, les structures mutables ne sont pas mauvaises, C # les a rendues mauvaises. J’utilise des structures mutables en C ++ tout le temps et elles sont très pratiques et intuitives. En revanche, C # m’a fait abandonner complètement les structures en tant que membres de classes en raison de la manière dont elles gèrent les objects. Leur commodité nous a coûté la nôtre.

Imaginez que vous ayez un tableau de 1 000 000 de structures. Chaque structure représentant une équité avec des choses comme bid_price, offer_price (peut-être décimales) et ainsi de suite, cela est créé par C # / VB.

Imaginez que ce tableau soit créé dans un bloc de mémoire alloué dans le segment de mémoire non géré, de sorte qu’un autre thread de code natif puisse accéder simultanément au tableau (peut-être un code de calcul très performant).

Imaginez que le code C # / VB écoute un stream de marché des modifications de prix, que ce code doive accéder à certains éléments du tableau (quelle que soit la sécurité), puis modifier certains champs de prix.

Imaginez que cela se fasse des dizaines, voire des centaines de milliers de fois par seconde.

Eh bien, admettons les faits, dans ce cas nous voulons vraiment que ces structures soient mutables, elles doivent l’être parce qu’elles sont partagées par un autre code natif, donc la création de copies ne va pas aider; ils doivent l’être parce que faire une copie d’une structure de 120 octets à ces taux est une folie, surtout lorsqu’une mise à jour peut avoir un impact sur un octet ou deux.

Hugo

Si vous vous en tenez à des structures (en C #, Visual Basic 6, Pascal / Delphi, struct type (ou classes)) lorsqu’elles ne sont pas utilisées comme pointeurs), vous constaterez qu’une structure n’est pas plus qu’une variable composée . Cela signifie que vous les traiterez comme un ensemble de variables, sous un nom commun (une variable d’enregistrement dont vous faites référence aux membres).

Je sais que cela dérouterait beaucoup de gens profondément habitués à la POO, mais ce n’est pas une raison suffisante pour dire que ces choses sont insortingnsèquement mauvaises si elles sont utilisées correctement. Certaines structures sont inaltérables comme elles l’entendent (c’est le cas du groupe namedtuple Python), mais c’est un autre paradigme à considérer.

Oui: les structs impliquent beaucoup de mémoire, mais ce ne sera pas précisément plus de mémoire en faisant:

 point.x = point.x + 1 

par rapport à:

 point = Point(point.x + 1, point.y) 

La consommation de mémoire sera au moins la même, voire plus dans le cas infime (bien que ce cas soit temporaire, pour la stack courante, en fonction de la langue).

Mais finalement, les structures sont des structures , pas des objects. Dans POO, la propriété principale d’un object est son identité , qui, la plupart du temps, ne dépasse pas son adresse mémoire. Struct est synonyme de structure de données (pas un object propre et ne possède donc aucune identité), et les données peuvent être modifiées. Dans d’autres langages, record (au lieu de struct , comme c’est le cas pour Pascal) est le mot et a le même objective: juste une variable d’enregistrement de données, destinée à être lue dans des fichiers, modifiée et transférée dans des fichiers (c’est le principal utiliser et, dans de nombreuses langues, vous pouvez même définir l’alignement des données dans l’enregistrement, alors que ce n’est pas nécessairement le cas pour les objects correctement appelés.

Vous voulez un bon exemple? Les structures permettent de lire facilement des fichiers. Python a cette librairie car, étant orientée object et ne supportant pas les structs, elle devait l’implémenter autrement, ce qui est un peu moche. Les langages implémentant les structures ont cette fonctionnalité … intégrée. Essayez de lire un en-tête bitmap avec une structure appropriée dans des langages tels que Pascal ou C. Ce sera facile (si la structure est correctement construite et alignée; en Pascal, vous n’utiliseriez pas un access basé sur des enregistrements mais des données binarys arbitraires). Ainsi, pour les fichiers et l’access direct (local) à la mémoire, les structures sont meilleures que les objects. As for today, we’re used to JSON and XML, and so we forget the use of binary files (and as a side effect, the use of structs). But yes: they exist, and have a purpose.

They are not evil. Just use them for the right purpose.

If you think in terms of hammers, you will want to treat screws as nails, to find screws are harder to plunge in the wall, and it will be screws’ fault, and they will be the evil ones.

When something can be mutated, it gains a sense of identity.

 struct Person { public ssortingng name; // mutable public Point position = new Point(0, 0); // mutable public Person(ssortingng name, Point position) { ... } } Person eric = new Person("Eric Lippert", new Point(4, 2)); 

Because Person is mutable, it’s more natural to think about changing Eric’s position than cloning Eric, moving the clone, and destroying the original . Both operations would succeed in changing the contents of eric.position , but one is more intuitive than the other. Likewise, it’s more intuitive to pass Eric around (as a reference) for methods to modify him. Giving a method a clone of Eric is almost always going to be surprising. Anyone wanting to mutate Person must remember to ask for a reference to Person or they’ll be doing the wrong thing.

If you make the type immutable, the problem goes away; if I can’t modify eric , it makes no difference to me whether I receive eric or a clone of eric . More generally, a type is safe to pass by value if all of its observable state is held in members that are either:

  • immutable
  • reference types
  • safe to pass by value

If those conditions are met then a mutable value type behaves like a reference type because a shallow copy will still allow the receiver to modify the original data.

The intuitiveness of an immutable Person depends on what you’re trying to do though. If Person just represents a set of data about a person, there’s nothing unintuitive about it; Person variables truly represent abstract values , not objects. (In that case, it’d probably be more appropriate to rename it to PersonData .) If Person is actually modeling a person itself, the idea of constantly creating and moving clones is silly even if you’ve avoided the pitfall of thinking you’re modifying the original. In that case it’d probably be more natural to simply make Person a reference type (that is, a class.)

Granted, as functional programming has taught us there are benefits to making everything immutable (no one can secretly hold on to a reference to eric and mutate him), but since that’s not idiomatic in OOP it’s still going to be unintuitive to anyone else working with your code.

It doesn’t have anything to do with structs (and not with C#, either) but in Java you might get problems with mutable objects when they are eg keys in a hash map. If you change them after adding them to a map and it changes its hash code , evil things might happen.

Personally when I look at code the following looks pretty clunky to me:

data.value.set ( data.value.get () + 1 ) ;

rather than simply

data.value++ ; or data.value = data.value + 1 ;

Data encapsulation is useful when passing a class around and you want to ensure the value is modified in a controlled fashion. However when you have public set and get functions that do little more than set the value to what ever is passed in, how is this an improvement over simply passing a public data structure around?

When I create a private structure inside a class, I created that structure to organize a set of variables into one group. I want to be able to modify that structure within the class scope, not get copies of that structure and create new instances.

To me this prevents a valid use of structures being used to organize public variables, if I wanted access control I’d use a class.

There are many advantages and disadvantages to mutable data. The million-dollar disadvantage is aliasing. If the same value is being used in multiple places, and one of them changes it, then it will appear to have magically changed to the other places that are using it. This is related to, but not identical with, race conditions.

The million-dollar advantage is modularity, sometimes. Mutable state can allow you to hide changing information from code that doesn’t need to know about it.

The Art of the Interpreter goes into these trade offs in some detail, and gives some examples.

There are several issues with Mr. Eric Lippert’s example. It is consortingved to illustrate the point that structs are copied and how that could be a problem if you are not careful. Looking at the example I see it as a result of a bad programming habit and not really a problem with either struct or the class.

  1. A struct is supposed to have only public members and should not require any encapsulation. If it does then it really should be a type/class. You really do not need two constructs to say the same thing.

  2. If you have class enclosing a struct, you would call a method in the class to mutate the member struct. This is what I would do as a good programming habit.

A proper implementation would be as follows.

 struct Mutable { public int x; } class Test { private Mutable m = new Mutable(); public int mutate() { mx = mx + 1; return mx; } } static void Main(ssortingng[] args) { Test t = new Test(); System.Console.WriteLine(t.mutate()); System.Console.WriteLine(t.mutate()); System.Console.WriteLine(t.mutate()); } 

It looks like it is an issue with programming habit as opposed to an issue with struct itself. Structs are supposed to be mutable, that is the idea and intent.

The result of the changes voila behaves as expected:

1 2 3 Press any key to continue . . .

I don’t believe they’re evil if used correctly. I wouldn’t put it in my production code, but I would for something like structured unit testing mocks, where the lifespan of a struct is relatively small.

Using the Eric example, perhaps you want to create a second instance of that Eric, but make adjustments, as that’s the nature of your test (ie duplication, then modifying). It doesn’t matter what happens with the first instance of Eric if we’re just using Eric2 for the remainder of the test script, unless you’re planning on using him as a test comparison.

This would be mostly useful for testing or modifying legacy code that shallow defines a particular object (the point of structs), but by having an immutable struct, this prevents it’s usage annoyingly.