web-dev-qa-db-fra.com

Pourquoi les structures mutables sont-elles «mauvaises»?

Suite aux discussions ici sur SO, j'ai déjà lu plusieurs fois la remarque selon laquelle les structures mutables sont "diaboliques" (comme dans la réponse à cette question ).

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

467
Dirk Vollmar

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

Ainsi, si vous modifiez une copie, vous ne modifiez que cette copie, pas l'original et aucune autre copie susceptible de se trouver.

Si votre structure est immuable, toutes les copies automatiques résultant du passage 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)

279
trampster

Par où commencer ;-p

blog d'Eric Lippert est toujours bon pour une citation:

C'est encore une autre raison pour laquelle les types de valeurs mutables sont diaboliques. Essayez de toujours rendre les types de valeur immuables.

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

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

qu'est-ce que cela a changé? Rien d'utile ...

La même chose avec les propriétés:

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

vous obliger à faire:

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

moins critique, il y a un problème de taille; les objets mutables ont tendance à avoir plusieurs propriétés; Cependant, si vous avez une structure avec deux ints, un string, 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).

165
Marc Gravell

Je ne dirais pas diabolique , mais la mutabilité est souvent un signe de la lourdeur excessive du programmeur qui fournit un maximum de fonctionnalités. En réalité, cela n'est souvent pas nécessaire, ce qui rend l'interface plus petite, plus facile à utiliser et plus difficile à utiliser incorrectement (= plus robuste).

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

J'affirme également que la mutabilité n'est presque jamais réellement nécessaire , le programmeur vient de penser qu'il pourrait être dans le futur. Par exemple, changer une date n'a aucun sens. Créez plutôt une nouvelle date à partir de l’ancienne. Il s’agit d’une opération peu coûteuse et la performance n’est donc pas prise en compte.

69
Konrad Rudolph

Les structures mutables 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 "le mal".

Je peux voir le fait que la syntaxe de C # ne permet pas de distinguer l'accès d'un membre d'un type de valeur ou d'un type de référence, je suis donc tout pour préférant structs immuable, qui imposent l'immuabilité sur mutable structs.

Cependant, au lieu de simplement étiqueter des structures immuables comme "diaboliques", je conseillerais d’adopter le langage et de préconiser une règle empirique plus utile et plus constructive.

Par exemple: "les structures 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 d'abord de travailler avec des structures en lecture seule".

43
JE42

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

Les méthodes de structure (par opposition aux utilisateurs de propriété) qui mutent "this" sont quelque peu pervers, 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 muent pas "this" devraient pouvoir être invoquées même sur des structures en lecture seule, sans avoir besoin d'une copie défensive. Les méthodes qui mutent "this" ne devraient pas du tout être invocables sur des structures en lecture seule. Puisque .net ne veut pas empêcher les méthodes de structure qui ne modifient pas "this" d'être invoqué sur des structures en lecture seule, mais qu'il ne veut pas autoriser la mutation de structures en lecture seule, il copie de manière défensive les structures en lecture. seuls contextes, obtenant sans doute le pire des deux mondes.

Malgré les problèmes posés par la manipulation de méthodes auto-mutantes dans des contextes en lecture seule, les structures mutables offrent souvent une sémantique bien supérieure aux types de classes mutables. Considérez les trois signatures de méthode suivantes:

 struct PointyStruct {public int x, y, z;}; 
 classe PointyClass {public int x, y, z;}; 
 
 void Méthode1 (PointyStruct foo); 
 void Method2 (réf. PointyStruct foo); 
 void Method3 (PointyClass foo); 

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

  1. En supposant que la méthode n'utilise aucun code "non sécurisé", pourrait-elle modifier foo?
  2. S'il n'existe aucune référence externe à 'foo' avant l'appel de la méthode, une référence externe peut-elle exister après?

Réponses:

Question 1:
Method1(): no (intention claire)
Method2(): yes (intention claire)
Method3(): yes (intention incertaine)
Question 2:
Method1(): no
Method2(): no (sauf si dangereuse)
Method3(): yes

Method1 ne peut pas modifier foo et n'obtient jamais de référence. Method2 obtient une référence éphémère à foo, qu'elle peut utiliser pour modifier les champs de foo autant de fois que nécessaire, dans n'importe quel ordre, jusqu'à ce qu'elle revienne, mais elle ne peut pas conserver cette référence. Avant le retour de Method2, sauf si elle utilise un code non sécurisé, toutes les copies éventuelles de sa référence "foo" auront disparu. Method3, contrairement à Method2, obtient une référence à foo qui peut être partagée à tout moment, et on ne sait pas ce que cela pourrait en faire. Cela ne changera peut-être pas du tout, cela changera et reviendrai, ou cela pourrait 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 objet de classe mutable passé dans celui-ci serait d'encapsuler l'objet mutable dans un wrapper en lecture seule, ce qui est laide et fastidieux.

Les tableaux de structures offrent une sémantique merveilleuse. Étant donné RectArray [500] de type Rectangle, il est clair et évident de savoir par exemple copiez l'élément 123 dans l'élément 456 puis définissez un peu plus tard la largeur de l'élément 123 sur 555, sans perturber l'élément 456. "RectArray [432] = RectArray [321]; ...; RectArray [123] .Width = 555;" . Le fait de savoir que Rectangle est une structure avec un champ entier appelé Width indiquera à tous ce qu’il faut savoir sur les instructions ci-dessus.

Supposons maintenant que RectClass est une classe avec les mêmes champs que Rectangle et qu'une autre souhaite effectuer les mêmes opérations sur un RectClassArray [500] de type RectClass. Peut-être que le tableau est supposé contenir 500 références immuables pré-initialisées à des objets 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 occurrences qui ne changeront pas, le code approprié serait plutôt "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 beaucoup plus sur RectClass (prend-il en charge, par exemple, un constructeur de copie, une méthode de copie, etc.) et sur l’utilisation prévue du tableau. Nulle part aussi propre que d'utiliser une structure.

Certes, il n’existe malheureusement aucun 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 indexer une collection avec, par exemple, une chaîne serait probablement de proposer 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 passé par référence au paramètre générique et à l'élément de collection. Cela permettrait à peu près la même sémantique que les tableaux de structure, mais à moins que les personnes vb.net et C # ne puissent être poursuivies pour offrir une syntaxe Nice, le code restera maladroit même s'il est raisonnablement performant (passer un paramètre générique permettre l’utilisation d’un délégué statique et éviterait de créer des instances de classe temporaires).

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

38
supercat

Les types de valeur représentent fondamentalement des concepts immuables. Fx, cela n’a aucun sens d’avoir une valeur mathématique telle qu’un entier, un vecteur, etc. et de pouvoir ensuite la modifier. Ce serait comme redéfinir le sens d'une valeur. Au lieu de modifier un type de valeur, il est plus logique d’attribuer 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, il s'agit de la même représentation universelle de cette valeur.

Comme le mentionne Konrad, il n'est pas judicieux de modifier une date non plus, car la valeur représente ce point unique dans le temps et non une instance d'objet de temps dépendant de l'état ou du contexte.

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

23
Morten Christiansen

Un autre cas de figure pourrait conduire à un comportement imprévisible du point de vue des programmeurs. Voici deux d'entre eux.

  1. Types de valeur immuable 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 compiler 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);
    }
}
 </ code>
  1. Types de valeurs mutables et tableau

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? Devrait-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<T> interface
IList<Mutable> 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);

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

18
Sergey Teplyakov

Si vous avez déjà programmé dans un langage comme C/C++, les structures sont très pratiques. Il suffit de les passer avec ref, autour et il n’ya rien qui puisse aller mal. Le seul problème que je trouve sont les restrictions du compilateur C # et que, dans certains cas, je ne peux pas forcer la chose stupide à utiliser une référence à la structure, à la place d'une copie (comme quand une structure fait partie d'une classe C # ).

Ainsi, les structures mutables ne sont pas mauvaises, C # les a rendues mauvaises. J'utilise tout le temps des structures mutables en C++ 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 à cause de la manière dont elles gèrent les objets. Leur commodité nous a coûté la nôtre.

17
ThunderGr

Si vous vous en tenez à ce que les structures sont destinées (en C #, Visual Basic 6, Pascal/Delphi, type de structure C++ (ou classes) lorsqu'elles ne sont pas utilisées en tant que pointeurs), vous constaterez qu'une structure - variable composée. Cela signifie que vous les traiterez comme un ensemble compact de variables, sous un nom commun (une variable d’enregistrement à partir de laquelle vous référencez des membres).

Je sais que cela dérouterait beaucoup de gens profondément habitués à la programmation orientée objet, mais ce n'est pas une raison suffisante pour dire que de telles choses sont intrinsèquement mauvaises, si elles sont utilisées correctement. Certaines structures sont immuables comme elles le souhaitent (c'est le cas de namedtuple de Python _), mais c'est un autre paradigme à considérer.

Oui: les structures 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 identique, voire supérieure dans le cas contraire (bien que ce soit temporaire, pour la pile actuelle, en fonction de la langue).

Mais, finalement, les structures sont structures, pas des objets. Dans POO, la propriété principale d’un objet est leur identité, qui, la plupart du temps, n’est pas supérieure à son adresse mémoire. Struct représente la structure de données (ce n'est donc pas un objet approprié et ils n'ont donc aucune identité) et les données peuvent être modifiées. Dans d'autres langues, record (au lieu de struct, comme c'est le cas pour Pascal) est le mot et a la même fonction: juste une variable d'enregistrement de données, destinée à être lue à partir de fichiers, modifiés et vidés dans des fichiers (c’est l’utilisation principale et, dans de nombreuses langues, vous pouvez même définir l’alignement des données dans l’enregistrement, bien que ce ne soit pas nécessairement le cas pour les objets proprement dits).

Vous voulez un bon exemple? Les structures sont utilisées pour lire les fichiers facilement. Python a cette bibliothèque car, comme il est orienté objet et qu'il ne prend pas en charge les structures, il a dû être implémenté d'une autre manière, ce qui est un peu moche. Les langages implémentant des 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. Cela sera facile (si la structure est correctement construite et alignée; en Pascal, vous n'utiliseriez pas un accès basé sur des enregistrements mais des fonctions permettant de lire des données binaires arbitraires). Ainsi, pour les fichiers et l’accès direct (local) à la mémoire, les structures sont meilleures que les objets. Pour ce qui est d’aujourd’hui, nous sommes habitués aux formats JSON et XML, nous oublions donc l’utilisation des fichiers binaires (et l’effet secondaire, l’utilisation des structs). Mais oui: ils existent et ont un but.

Ils ne sont pas mauvais. Il suffit de les utiliser pour le bon but.

Si vous pensez en termes de marteaux, vous voudrez traiter les vis comme des clous, trouver des vis plus difficiles à plonger dans le mur, et ce sera la faute des vis, et ce seront les mauvaises.

12
Luis Masuelli

Imaginez que vous ayez un tableau de 1 000 000 de structures. Chaque structure représentant une équité avec des éléments tels que bid_price, offer_price (peut-être des nombres décimaux), etc., est créée 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 du code très performant faisant des calculs mathématiques).

Imaginez que le code C #/VB écoute les modifications de prix sur le marché, que ce code doive parfois accéder à un élément du tableau (quelle que soit la sécurité), puis modifier un ou plusieurs champs de prix.

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

Voyons les faits en face. 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, de sorte que la création de copies ne va pas aider; ils doivent l'être, car faire une copie d'une structure d'environ 120 octets à ces taux est une folie, en particulier lorsqu'une mise à jour peut en réalité affecter un octet ou deux.

Hugo

12
Hugo

Lorsque quelque chose peut être muté, il acquiert un sens d'identité.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Parce que Person est mutable, il est plus naturel de penser à changer la position d'Eric à cloner Eric, déplacer le clone et détruire l'original. Les deux opérations réussiraient à changer le contenu de eric.position, mais l’une est plus intuitive que l’autre. De même, il est plus intuitif de faire passer Eric (comme référence) aux méthodes pour le modifier. Donner à une méthode un clone d'Eric sera presque toujours surprenant. Toute personne souhaitant muter Person doit se rappeler de demander une référence à Person sinon elle agira de la mauvaise façon.

Si vous rendez le type immuable, le problème disparaîtra; si je ne peux pas modifier eric, le fait que je reçoive eric ou un clone de eric ne me change rien. Plus généralement, un type est sûr de passer par valeur si tout son état observable est conservé dans des membres qui sont:

  • immuable
  • types de référence
  • sûr de passer par valeur

Si ces conditions sont remplies, un type de valeur mutable se comporte comme un type de référence car une copie superficielle autorisera néanmoins le destinataire à modifier les données d'origine.

L'intuitivité d'une immuable Person dépend de ce que vous essayez de faire. Si Person représente simplement un ensemble de données à propos d'une personne, il n'y a rien d'intelligent à ce sujet; Person les variables représentent réellement abstrait valeurs, pas des objets. (Dans ce cas, il serait probablement plus approprié de le renommer en PersonData.) Si Person est en train de modéliser une personne elle-même, l'idée de créer et de déplacer constamment des clones est stupide même si vous ' Nous avons évité le piège de penser que vous modifiez l’original. Dans ce cas, il serait probablement plus naturel de faire simplement Person un type de référence (c'est-à-dire une classe.)

Certes, la programmation fonctionnelle nous a appris qu’il ya des avantages à rendre tout immuable (personne ne peut garder secrètement une référence à eric et le muter), mais puisque cela n’est pas idiomatique en OOP il ne sera toujours pas intuitif pour quiconque de travailler avec votre code.

8
Doval

Personnellement, lorsque je regarde le code, les éléments suivants me semblent assez maladroits:

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

plutôt que simplement

data.value ++; ou data.value = data.value + 1;

L'encapsulation de données est utile lorsque vous passez une classe et que vous voulez vous assurer que la valeur est modifiée de manière contrôlée. Cependant, lorsque vous avez un ensemble public et des fonctions qui ne font que définir la valeur à ce qui est transmis, en quoi est-ce une amélioration par rapport à la simple transmission d'une structure de données publique?

Lorsque je crée une structure privée dans une classe, j'ai créé cette structure pour organiser un ensemble de variables en un groupe. Je veux pouvoir modifier cette structure dans la portée de la classe, ne pas en obtenir des copies et créer de nouvelles instances.

Pour moi, cela empêche une utilisation valide des structures utilisées pour organiser les variables publiques. Si je voulais contrôler l'accès, j'utiliserais une classe.

6
Mike

L'exemple de M. Eric Lippert pose plusieurs problèmes. Il est artificiel pour illustrer le fait que les structures sont copiées et comment cela pourrait poser problème si vous ne faites pas attention. En regardant l'exemple, je le vois comme le résultat d'une mauvaise habitude de programmation et pas vraiment d'un problème avec struct ou la classe.

  1. Une structure est supposée avoir uniquement des membres publics et ne devrait nécessiter aucune encapsulation. Si c'est le cas, il devrait s'agir d'un type/classe. Vous n'avez vraiment pas besoin de deux constructions pour dire la même chose.

  2. Si vous avez une classe contenant une structure, appelez une méthode de la classe pour transformer la structure du membre. C’est ce que je ferais comme bonne habitude de programmation.

Une mise en œuvre appropriée serait la suivante.

struct Mutable {
public int x;
}

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

Il semble que ce soit un problème d'habitude de programmation par opposition à un problème de struct lui-même. Les structures sont supposées être mutables, c'est l'idée et l'intention.

Le résultat des changements voila se comporte comme prévu:

1 2 3 Appuyez sur n’importe quelle touche pour continuer. . .

6
Ramesh Kadambi

Cela n’a rien à voir avec les structures (ni avec C #), mais dans Java, vous pourriez avoir des problèmes avec les objets mutables quand ils sont, par exemple. clés dans une carte de hachage. Si vous les changez après les avoir ajoutés à une carte et que celle-ci change code de hachage , des événements pervers pourraient se produire.

6
Bombe

Les données mutables présentent de nombreux avantages et inconvénients. Le désavantage d'un million de dollars est un aliasing. Si la même valeur est utilisée à plusieurs endroits et que l'un d'entre eux la modifie, elle semblera s'être modifiée comme par magie sur les autres endroits qui l'utilisent. Ceci est lié aux conditions de concurrence, mais pas identique à celles-ci.

L'avantage d'un million de dollars est la modularité, parfois. L'état mutable peut vous permettre de cacher des informations changeantes au code qui n'a pas besoin de le savoir.

L'art de l'interprète décrit en détail ces compromis et donne quelques exemples.

5
Andru Luvisi

Je ne crois pas qu'ils soient pervers s'ils sont utilisés correctement. Je ne le mettrais pas dans mon code de production, mais je le ferais pour quelque chose comme les tests unitaires structurés, où la durée de vie d'une structure est relativement courte.

En utilisant l'exemple d'Eric, vous voudrez peut-être créer une deuxième instance de cet Eric, mais apportez des ajustements, en fonction de la nature de votre test (c'est-à-dire la duplication, puis la modification). Peu importe ce qui se passe avec la première instance d'Eric si nous utilisons simplement Eric2 pour le reste du script de test, à moins que vous ne prévoyiez de l'utiliser comme comparaison de test.

Cela serait surtout utile pour tester ou modifier le code existant qui définit peu profondément un objet particulier (le point de struct), mais en ayant une structure immuable, cela empêche son utilisation de manière ennuyeuse.

1
ShaunnyBwoy