Question

À la suite des discussions ici sur SO, je lisais déjà plusieurs fois la remarque que les structures mutables sont «diaboliques» (comme dans la réponse à cette question question ).

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

Était-ce utile?

La solution

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 ni aucune autre copie susceptible de circuler.

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)

Autres conseils

Par où commencer ;-p

Le blog d'Eric Lippert est toujours bon pour un devis:

  

C’est encore une autre raison pour laquelle mutable   les types de valeur 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 ...

Idem avec les propriétés:

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

vous obligeant à 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 à posséder plusieurs propriétés; Si vous avez une structure avec deux int , une chaîne , 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 diabolique mais la mutabilité est souvent un signe de la lourdeur excessive du programmeur pour fournir 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 mal (= plus robuste).

Un exemple en 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.

De plus, je prétends que la mutabilité est presque jamais réellement , le programmeur pense seulement que cela pourrait être dans l'avenir. 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.

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 "mal".

Je vois bien 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 à fait favorable à préférer des structures immuables, qui imposent l'immuabilité. , sur des structures mutables.

Cependant, au lieu de simplement étiqueter des structures immuables comme étant "pervers", 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 de travailler d'abord avec les structures en lecture seule" .

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

Méthodes de structure (distinctes des propriétés de propriété) qui mutent "this" sont quelque peu pervers, uniquement parce que .net ne fournit pas un moyen de les distinguer des méthodes qui ne le font pas. Méthodes de structure qui ne mutent pas "this" devrait être invocable même sur des structures en lecture seule sans avoir besoin d'une copie défensive. Les méthodes qui mutent "ceci" ne devrait pas être invocable du tout sur les structures en lecture seule. Étant donné que .net ne veut pas interdire les méthodes de structure qui ne modifient pas "cela" d'être invoqué sur des structures en lecture seule, mais ne veut pas autoriser la mutation de structures en lecture seule, il copie de manière défensive les structures dans des contextes en lecture seule, obtenant 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 classes mutables. Considérez les trois signatures de méthode suivantes:

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

void Method1(PointyStruct foo);
void Method2(ref PointyStruct foo);
void Method3(PointyClass foo);

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

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

Réponses:

  

Question 1:
 & # 8195; Method1 () : non (intention claire)
 & # 8195; Method2 () : oui (intention claire)
 & # 8195; Method3 () : oui (intention incertaine)
 Question 2:
 & # 8195; Method1 () : non
 & # 8195; Method2 () : non (sauf en cas de danger)
 & # 8195; Method3 () : oui

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. Le seul moyen 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 encombrant.

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 sur 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 soit une classe avec les mêmes champs que Rectangle et qu’elle 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é ressemblerait davantage à " RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321]); RectClassArray [321] .X = 555; " Pour savoir ce qu’on est censé faire, il faudrait en savoir beaucoup plus sur RectClass (e.

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 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 ne sert à rien de changer une date non plus, car la valeur représente ce moment unique et non une instance d'objet de temps dépendant de l'état ou du contexte.

Espère que cela a un 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.

Un autre cas de figure pourrait conduire à un comportement imprévisible du point de vue des programmeurs. En voici quelques uns.

  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);
    }
}

  1. Types et tableau de valeurs 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? 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.

Si vous avez déjà programmé dans un langage tel que C / C ++, les structures sont pratiques à utiliser comme mutables. 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 faites mal. 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.

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 décimales), 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).

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

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

Eh bien, regardons 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 seulement.

Hugo

Si vous vous en tenez à ce à quoi 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 comme pointeurs), vous constaterez qu'une structure ne dépasse pas une 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 il s'agit d'un autre paradigme à prendre en compte.

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

point.x = point.x + 1

comparé à:

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 en cours, en fonction de la langue).

Mais, finalement, les structures sont des 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 pas un objet approprié, et ils n'ont donc aucune identité), et les données peuvent être modifiées. Dans d’autres langues, enregistrement (au lieu de struct , comme c’est le cas pour Pascal) est le mot et a le même objectif: il s’agit simplement d’une variable d’enregistrement de données destinée à être lue. à partir de fichiers, modifiés et exporté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 nommés).

Vous voulez un bon exemple? Les structures sont utilisées pour lire les fichiers facilement. Python a cette bibliothèque car, puisqu'elle est orientée objet et ne prend pas en charge les structs, elle devait mettez-le en œuvre d'une autre manière, 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. Cela sera facile (si la structure est correctement construite et alignée; en Pascal, vous n'utiliseriez pas un accès basé sur un enregistrement 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 une raison d'être.

Ils ne sont pas mauvais. Utilisez-les simplement pour le bon but.

Si vous pensez en termes de marteaux, vous voudrez traiter les vis comme des clous, il est plus difficile de trouver des vis qui plonge dans le mur, et ce sera la faute des vis, et ce seront les méchantes.

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));

Etant donné que Personne est modifiable, il est plus naturel de penser à changer la position d'Eric qu'à 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 Personne doit se rappeler de demander une référence à Personne , sinon elle agira de la mauvaise manière.

Si vous rendez le type immuable, le problème disparaît; 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
  • safe to pass by value

Si ces conditions sont remplies, un type de valeur modifiable 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 Personne immuable dépend de ce que vous essayez de faire. Si Personne représente simplement un ensemble de données sur une personne, il n’ya rien d’inconvénient; Les variables Person représentent réellement des valeurs abstraites , et non 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 en permanence des clones C'est idiot, même si vous avez évité le piège de penser que vous modifiez l'original. Dans ce cas, il serait probablement plus naturel de simplement faire de Person un type de référence (c'est-à-dire une classe.)

Certes, la programmation fonctionnelle nous a appris qu'il était avantageux de rendre tout immuable (personne ne peut garder secrètement une référence à eric et le muter), mais étant donné que ce n'est pas idiomatique en POO, cela ne sera toujours pas intuitif pour quiconque travaille avec votre code.

Cela n'a rien à voir avec les structures (et non plus avec C #), mais en Java, vous pouvez rencontrer des problèmes avec les objets mutables lorsqu'ils sont, par exemple. clés dans une carte de hachage. Si vous les modifiez après les avoir ajoutés à une carte et si elle change code de hachage , des événements néfastes pourraient se produire.

Personnellement, lorsque je regarde le code, les éléments suivants me semblent plutôt 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 souhaite pouvoir modifier cette structure dans le cadre de la classe, ne pas en obtenir des copies et créer de nouvelles instances.

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

L’exemple de M. Eric Lippert pose plusieurs problèmes. Il est conçu 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 la classe inclut une structure, vous appelleriez une méthode de la classe pour muter 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 modifications voila se comporte comme prévu:

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

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 avoir changé comme par magie aux autres endroits qui l'utilisent. Ceci est lié aux conditions de concurrence, mais pas identique.

L’avantage d’un million de dollars est parfois la modularité. 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.

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 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 (duplication puis 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 l’utiliser comme comparaison de test.

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top