Pourquoi les structures mutables sont-elles «mauvaises»?


485

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 avec la mutabilité et les structures en C #?


12
Affirmer que les structures mutables sont mauvaises, c'est comme affirmer que les ints mutables , les bools et tous les autres types de valeurs sont mauvais. Il existe des cas de mutabilité et d'immuabilité. Ces cas dépendent du rôle que jouent les données, et non du type d'allocation / partage de mémoire.
Slipp D. Thompson

41
@slipp intet nebool sont pas mutables ..
Blorgbeard est sorti

2
.-Syntax, rendant les opérations avec des données de type ref et des données de type valeur identiques, même si elles sont distinctement différentes. C'est une faute des propriétés de C #, pas des structures - certains langages offrent une a[V][X] = 3.14syntaxe alternative pour muter sur place. En C #, vous feriez mieux d'offrir des méthodes de mutateur struct-membre comme 'MutateV (Action <ref Vector2> mutator)' et de l'utiliser comme a.MutateV((v) => { v.X = 3; }) (l'exemple est trop simplifié en raison des limitations que C # a concernant le refmot - clé, mais avec certains des solutions de contournement devraient être possibles) .
Slipp D. Thompson

2
@Slipp Eh bien, je pense exactement à l'opposé de ce genre de structures. Pourquoi pensez-vous que les structures qui sont déjà implémentées dans la bibliothèque .NET, comme DateTime ou TimeSpan (si similaires) sont immuables? Peut-être qu'il pourrait être utile de changer un seul membre de la var de cette structure, mais c'est tout simplement trop gênant, conduit à trop de problèmes. En fait, vous vous trompez sur ce que calcule le processeur, car C # ne compile pas en assembleur, il compile en IL. En IL (à condition que nous ayons déjà la variable nommée x), cette opération unique est de 4 instructions: ldloc.0(charge la variable 0-index dans ...
Sushi271

1
... type. Test le type. Ref est juste un mot clé qui fait passer une variable à une méthode elle-même, pas une copie de celle-ci. Il a également un sens pour les types de référence, car nous pouvons changer la variable , c'est-à-dire que la référence en dehors de la méthode pointera vers un autre objet après avoir été modifiée dans la méthode. Puisqu'il ref Tne s'agit pas d'un type, mais de la façon de passer un paramètre de méthode, vous ne pouvez pas le placer <>, car seuls les types peuvent y être placés. C'est donc tout simplement incorrect. Peut-être que ce serait pratique de le faire, peut-être que l'équipe C # pourrait le faire pour une nouvelle version, mais en ce moment ils travaillent sur certains ...
Sushi271

Réponses:


290

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 être présentes.

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)


82
"Si votre structure est immuable, alors toutes les copies seront les mêmes." Non, cela signifie que vous devez consciemment faire une copie si vous voulez une valeur différente. Cela signifie que vous ne serez pas surpris de modifier une copie en pensant que vous modifiez l'original.
Lucas

19
@Lucas Je pense que vous parlez d'un autre type de copie, je parle des copies automatiques faites suite à leur passage par valeur ce n'est pas vraiment une copie, ce sont de nouveaux instants délibérés contenant des données différentes.
trampster

3
Votre montage (16 mois plus tard) rend cela un peu plus clair. Je reste par contre "(structure immuable) signifie que vous ne serez pas pris en train de modifier une copie en pensant que vous modifiez l'original".
Lucas

6
@Lucas: Le danger de faire une copie d'une structure, de la modifier, et de penser que l'on modifie en quelque sorte l'original (lorsque le fait d'écrire un champ struct rend évident le fait que l'on n'écrit que sa copie) semble assez petit par rapport au danger que quelqu'un qui détient un objet de classe comme moyen de conserver les informations qu'il contient mute l'objet pour mettre à jour ses propres informations et, dans le processus, corrompre les informations détenues par un autre objet.
supercat

2
Le troisième paragraphe semble au mieux faux ou peu clair. Si votre structure est immuable, vous ne pourrez tout simplement pas modifier ses champs ou les champs des copies effectuées. « Si vous voulez changer , vous devez ... » qui est trompeur aussi, vous ne pouvez pas changer ce jamais , ni consciemment , ni inconsciemment. La création d'une nouvelle instance dont les données souhaitées n'ont rien à voir avec la copie d'origine, à part avoir la même structure de données.
Saeb Amini

167

Par où commencer ;-p

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

C'est encore 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 assez facilement les modifications ... par exemple, retirer 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 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 à avoir de multiples propriétés; pourtant, si vous avez une structure avec deux ints, a string, a DateTimeet a 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).


5
Eh bien oui, mais le compilateur est juste stupide de cette façon. Ne pas autoriser l'attribution aux membres de la structure de propriété était à mon humble avis une décision de conception stupide, car elle est autorisée pour l' ++opérateur. Dans ce cas, le compilateur écrit simplement l'affectation explicite elle-même au lieu de bousculer le programmeur.
Konrad Rudolph

12
@Konrad: myObj.SomeProperty.Size = 22 modifierait une COPIE de myObj.SomeProperty. Le compilateur vous sauve d'un bug évident. Et ce n'est PAS autorisé pour ++.
Lucas

@Lucas: Eh bien, le compilateur Mono C # le permet certainement - puisque je n'ai pas Windows, je ne peux pas vérifier le compilateur de Microsoft.
Konrad Rudolph

6
@Konrad - avec une indirection en moins, cela devrait fonctionner; c'est la "mutation d'une valeur de quelque chose qui n'existe que comme valeur transitoire sur la pile et qui est sur le point de s'évaporer dans le néant" qui est le cas qui est bloqué.
Marc Gravell

2
@Marc Gravell: Dans l'ancien morceau de code, on se retrouve avec un "Foo" dont le nom est "abc" et dont les autres attributs sont ceux de List [0], sans déranger List [0]. Si Foo était une classe, il serait nécessaire de la cloner puis de modifier la copie. À mon avis, le gros problème avec la distinction valeur-type vs classe est l'utilisation du "." opérateur à deux fins. Si j'avais mes druthers, les cours pourraient supporter les deux "." et "->" pour les méthodes et propriétés, mais la sémantique normale pour "." propriétés serait de créer une nouvelle instance avec le champ approprié modifié.
supercat

71

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

Un exemple de ceci est les conflits de lecture / écriture et d'écriture / écriture dans les conditions de concurrence. Ceux-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é n'est presque jamais réellement nécessaire , le programmeur pense simplement que cela pourrait être à l'avenir. Par exemple, il n'est tout simplement pas logique de modifier une date. Créez plutôt une nouvelle date basée sur l'ancienne. Il s'agit d'une opération bon marché, donc les performances ne sont pas prises en compte.


1
Eric Lippert dit qu'ils sont ... voyez ma réponse.
Marc Gravell

42
Bien que je respecte Eric Lippert, il n'est pas Dieu (ou du moins pas encore). Le blog que vous liez et votre article ci-dessus sont des arguments raisonnables pour rendre les structures immuables, bien sûr, mais ils sont en fait très faibles comme arguments pour ne jamais utiliser de structures mutables. Ce message, cependant, est un +1.
Stephen Martin

2
En développant en C #, vous avez généralement besoin de mutabilité de temps en temps - en particulier avec votre modèle d'entreprise, où vous souhaitez que le streaming, etc. fonctionne en douceur avec les solutions existantes. J'ai écrit un article sur la façon de travailler avec des données mutables ET immuables, résolvant la plupart des problèmes de mutabilité (j'espère): rickyhelgesson.wordpress.com/2012/07/17/…
Ricky Helgesson

3
@StephenMartin: Les structures qui encapsulent souvent une seule valeur doivent être immuables, mais les structures sont de loin le meilleur moyen d'encapsuler des ensembles fixes de variables indépendantes mais liées (comme les coordonnées X et Y d'un point) qui n'ont pas d '"identité" en tant que groupe. Les structures qui sont utilisées à cet effet devraient généralement exposer leurs variables en tant que champs publics. Je considère que l'idée qu'il est plus approprié d'utiliser une classe qu'une structure à de telles fins est tout simplement fausse. Les classes immuables sont souvent moins efficaces et les classes mutables ont souvent une sémantique épouvantable.
supercat

2
@StephenMartin: Considérons, par exemple, une méthode ou une propriété qui est censée renvoyer les six floatcomposants d'une transformation graphique. Si une telle méthode renvoie une structure de champ exposé avec six composants, il est évident que la modification des champs de la structure ne modifiera pas l'objet graphique à partir duquel elle a été reçue. Si une telle méthode retourne un objet de classe mutable, peut-être que changer ses propriétés changera l'objet graphique sous-jacent et peut-être pas - personne ne sait vraiment.
supercat

48

Les structures mutables ne sont pas mauvaises.

Ils sont absolument nécessaires dans des circonstances 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 point que la syntaxe de C # n'aide pas à distinguer l'accès d'un membre d'un type valeur ou d'un type référence, donc je suis tout à fait pour préférer les structures immuables, qui imposent l'immuabilité, aux structures mutables.

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

Par exemple: "les structures sont des types de valeurs, qui sont copiées par défaut. Vous avez besoin d'une référence si vous ne voulez pas les copier" ou "essayez de travailler avec des structures en lecture seule" .


8
Je dirais également que si l'on veut fixer un ensemble fixe de variables avec du ruban adhésif pour que leurs valeurs puissent être traitées ou stockées séparément ou en tant qu'unité, il est beaucoup plus logique de demander au compilateur de fixer un ensemble fixe de ensemble (c.-à-d. déclarer un structavec des champs publics) que de définir une classe qui peut être utilisée, maladroitement, pour atteindre les mêmes fins, ou pour ajouter un tas d'ordure à une structure pour la faire émuler une telle classe (plutôt que de l'avoir se comporter comme un ensemble de variables collées ensemble avec du ruban adhésif, ce que l'on veut vraiment en premier lieu)
supercat

41

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

Les méthodes Struct (distinctes des setters de propriétés) qui mutent "this" sont quelque peu mauvaises, uniquement parce que .net ne fournit pas un moyen de les distinguer des méthodes qui ne le font pas. Les méthodes de structure qui ne mutent pas "ceci" devraient être invocables même sur des structures en lecture seule sans avoir besoin de copie défensive. Les méthodes qui mutent "this" ne devraient pas du tout être invocables sur des structures en lecture seule. Étant donné que .net ne veut pas interdire aux méthodes de 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 dans read- seulement des contextes, obtenant sans doute le pire des deux mondes.

Malgré les problèmes de gestion des méthodes d'auto-mutation dans des contextes en lecture seule, cependant, 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 code "dangereux", pourrait-elle 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 danger)
Method3() : oui

La méthode 1 ne peut pas modifier foo et n'obtient jamais de référence. La méthode 2 obtient une référence de courte durée à foo, qu'elle peut utiliser pour modifier les champs de foo un certain nombre de fois, 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, à moins qu'il n'utilise du code non sécurisé, toutes les copies qui auraient pu être faites de sa référence 'foo' auront disparu. La méthode 3, à la différence de la méthode 2, obtient une référence partageable de manière prometteuse à foo, et on ne sait pas ce qu'elle pourrait en faire. Il pourrait ne pas changer du tout foo, il pourrait changer foo puis revenir, ou il pourrait donner une référence à foo à un autre thread qui pourrait le muter d'une manière arbitraire à un moment futur arbitraire.

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 sur 555, sans déranger 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 le monde ce qu'il faut savoir sur les instructions ci-dessus.

Supposons maintenant 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. Peut-être que le tableau est censé 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 instances qui ne vont pas changer, donc le code approprié ressemblerait plus à "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 (par exemple, prend-il en charge un constructeur de copie, une méthode de copie depuis, etc. ) et l'utilisation prévue de la baie. Nulle part aussi propre que d'utiliser une structure.

Pour être sûr, il n'y a 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 veut qu'une collection soit indexée 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 à la fois au paramètre générique et à l'élément de collection. Cela permettrait presque la même sémantique que les tableaux de structures, mais à moins que les personnes vb.net et C # ne puissent être poursuivies pour offrir une belle syntaxe, le code va être maladroit même s'il est raisonnablement performant (le passage d'un paramètre générique serait permettre l'utilisation d'un délégué statique et éviterait la nécessité de créer des instances de classe temporaires).

Personnellement, je suis irrité par 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 mutables sont mieux adaptés que tout autre type d'entité.


1
@ Ron Warholic: il n'est pas évident que SomeRect soit un rectangle. Il peut s'agir d'un autre type qui peut être implicitement transtypé à partir de Rectangle. Cependant, le seul type défini par le système qui peut être implicitement transtypé à partir de Rectangle est RectangleF, et le compilateur squawk si on essayait de passer les champs d'un RectangleF au constructeur de Rectangle (puisque les premiers sont Single, et le dernier Integer) , il pourrait y avoir des structures définies par l'utilisateur qui permettent de telles transtypages implicites. BTW, la première instruction fonctionnerait tout aussi bien que SomeRect soit un Rectangle ou un RectangleF.
supercat

Tout ce que vous avez montré, c'est que dans un exemple artificiel, vous pensez qu'une méthode est plus claire. Si nous prenons votre exemple avec, Rectangleje pourrais facilement trouver une position commune où vous obtenez un comportement très peu clair . Considérez que WinForms implémente un Rectangletype mutable utilisé dans la Boundspropriété du formulaire . Si je veux changer les limites, je voudrais utiliser votre belle syntaxe: form.Bounds.X = 10;Cependant, cela ne change précisément rien sur le formulaire (et génère une belle erreur vous en informant). L'incohérence est le fléau de la programmation et c'est pourquoi l'immuabilité est souhaitée.
Ron Warholic

3
@Ron Warholic: BTW, je voudrais pouvoir dire "form.Bounds.X = 10;" et le faire fonctionner, mais le système ne fournit aucun moyen propre de le faire. Une convention pour exposer des propriétés de type valeur en tant que méthodes acceptant les rappels pourrait offrir un code beaucoup plus propre, efficace et correctement confirmé que toute approche utilisant des classes.
supercat

4
Cette réponse est beaucoup plus perspicace que quelques-unes des réponses les plus votées. Il est en quelque sorte absurde que l'argument contre les types de valeurs mutables repose sur la notion de "ce que vous attendez" qui se produise lorsque vous mélangez l'aliasing et la mutation. C'est une chose terrible à faire de toute façon !
Eamon Nerbonne

2
@supercat: Qui sait, peut-être que la fonction de retour de référence dont ils parlent pour C # 7 pourrait couvrir cette base (je ne l'ai pas vraiment examinée en détail, mais elle semble superficiellement similaire).
Eamon Nerbonne

24

Les types de valeurs 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 ensuite de pouvoir 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 valeurs 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, alors c'est la même représentation universelle de cette valeur.

Comme Konrad le mentionne, cela n'a pas de sens de changer une date non plus, car la valeur représente ce point unique dans le temps et non une instance d'un objet temporel qui a un état ou une dépendance au contexte.

J'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.


Eh bien, ils devraient représenter des concepts immuables, au moins ;-p
Marc Gravell

3
Eh bien, je suppose qu'ils auraient pu rendre System.Drawing.Point immuable, mais cela aurait été une grave erreur de conception à mon humble avis. Je pense que les points sont en fait un type de valeur archétypique et qu'ils sont mutables. Et ils ne posent aucun problème à quiconque au-delà de la programmation très précoce pour les débutants.
Stephen Martin

3
En principe, je pense que les points devraient également être immuables, mais si cela rend le type plus difficile ou moins élégant à utiliser, cela doit bien sûr être pris en compte également. Il ne sert à rien d'avoir des constructions de code qui respectent les principes les plus fins si personne ne veut les utiliser;)
Morten Christiansen

3
Les types de valeurs sont utiles pour représenter des concepts immuables simples, mais les structures à champ exposé sont les meilleurs types à utiliser pour contenir ou faire circuler de petits ensembles fixes de valeurs liées mais indépendantes (telles que les coordonnées d'un point). Un emplacement de stockage d'un tel type de valeur encapsule les valeurs de ses champs et rien d'autre. En revanche, un emplacement de stockage d'un type de référence mutable peut être utilisé dans le but de conserver l'état de l'objet mutable, mais encapsule également l'identité de toutes les autres références dans l'univers qui existent pour ce même objet.
supercat

4
«Les types de valeur représentent essentiellement des concepts immuables». Non, ils ne le font pas. L'une des applications les plus anciennes et les plus utiles d'une variable de type valeur est un intitérateur, qui serait complètement inutile s'il était immuable. Je pense que vous confondez les «implémentations du compilateur / runtime des types de valeurs» avec des «variables typées en un type de valeur» - ce dernier est certainement modifiable en n'importe laquelle des valeurs possibles.
Slipp D. Thompson

19

Il y a quelques autres cas d'angle qui pourraient conduire à un comportement imprévisible du point de vue du programmeur.

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

Types et tableau de valeurs mutables

Supposons que nous ayons un tableau de notre Mutablestructure et que nous appelions la IncrementIméthode 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<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 mutables 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.


5
Que se passerait-il, si les langages .net avaient un support de type valeur légèrement meilleur, les méthodes struct devraient être interdites de muter 'this' à moins qu'elles ne soient explicitement déclarées comme telles, et les méthodes ainsi déclarées devraient être interdites en lecture seule contextes. Les tableaux de structures mutables offrent une sémantique utile qui ne peut pas être efficacement réalisée par d'autres moyens.
supercat

2
ce sont de bons exemples de problème très subtil qui résulterait de structures mutables. Je ne m'attendais à aucun de ces comportements. Pourquoi un tableau vous donnerait une référence, mais une interface vous donnerait une valeur? J'aurais pensé, à part les valeurs tout le temps (ce à quoi je m'attendais vraiment), que ce serait au moins l'inverse: interface donnant des références; tableaux donnant des valeurs ...
Dave Cousineau

@Sahuagin: Malheureusement, il n'y a pas de mécanisme standard permettant à une interface d'exposer une référence. Il y a des façons. .Net pourrait permettre que de telles choses soient faites en toute sécurité et utilement (par exemple en définissant une structure spéciale "ArrayRef <T>" contenant un T[]et un index entier, et en prévoyant qu'un accès à une propriété de type ArrayRef<T>sera interprété comme un accès à l'élément de tableau approprié) [si une classe voulait exposer un ArrayRef<T>dans un autre but, elle pourrait fournir une méthode - par opposition à une propriété - pour le récupérer]. Malheureusement, de telles dispositions n'existent pas.
supercat

2
Oh mon ... cela rend les structures mutables sacrément diaboliques!
nawfal

1
J'aime cette réponse car elle contient des informations très précieuses qui ne sont pas évidentes. Mais vraiment, ce n'est pas un argument contre les structures mutables comme certains le prétendent. Oui, ce que nous voyons ici est un "gouffre de désespoir" comme l'aurait dit Eric, mais la source de ce désespoir n'est pas la mutabilité. La source du désespoir réside dans les méthodes d' auto-mutation des structures . (Quant aux raisons pour lesquelles les tableaux et les listes se comportent différemment, c'est parce que l'un est essentiellement un opérateur qui calcule une adresse mémoire et l'autre est une propriété. En général, tout devient clair une fois que vous comprenez qu'une "référence" est une valeur d' adresse .)
AnorZaken

18

Si vous avez déjà programmé dans un langage comme C / C ++, les structures peuvent être utilisées comme mutables. Il suffit de les passer avec ref, autour et il n'y a rien qui puisse mal tourner. Le seul problème que je trouve est les restrictions du compilateur C # et que, dans certains cas, je suis incapable de 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 # ).

Ainsi, 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 façon dont elles gèrent les objets. Leur commodité nous a coûté la nôtre.


Avoir des champs de classe de types de structure peut souvent être un modèle très utile, bien qu'il y ait certes quelques limites. Les performances seront dégradées si l'on utilise des propriétés plutôt que des champs ou des utilisations readonly, mais si l'on évite de faire ces choses, les champs de classe des types de structure sont très bien. La seule limitation vraiment fondamentale des structures est qu'un champ struct d'un type de classe mutable comme int[]peut encapsuler l'identité ou un ensemble de valeurs immuable, mais ne peut pas être utilisé pour encapsuler des valeurs mutables sans encapsuler également une identité indésirable.
supercat

13

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) quand 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 complet de variables, sous un nom commun (une variable d'enregistrement à partir de laquelle vous référencez les 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 de telles choses sont intrinsèquement mauvaises, si elles sont utilisées correctement. Certaines structures sont immuables comme elles l'entendent (c'est le cas de Pythonnamedtuple ), 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 la même, voire plus dans le cas inmutable (bien que ce cas soit temporaire, pour la pile actuelle, selon la langue).

Mais, finalement, les structures sont des structures , pas des objets. Dans POO, la propriété principale d'un objet est son identité , qui la plupart du temps n'est pas plus que son adresse mémoire. Struct est synonyme de structure de données (pas un objet approprié, et ils n'ont donc pas d'identité de toute façon), 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 le même but: juste une variable d'enregistrement de données, destinée à être lue à partir de fichiers, modifiée et sauvegardé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 objets correctement appelés).

Vous voulez un bon exemple? Les structures sont utilisées pour lire facilement les fichiers. Python possède cette bibliothèque car, étant donné qu'elle est orientée objet et ne prend pas en charge les structures, elle a dû l'implémenter d'une autre manière, ce qui est quelque 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 comme Pascal ou C.Ce 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 pour 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. Aujourd'hui, nous sommes habitués au JSON et au XML, et nous oublions donc l'utilisation de fichiers binaires (et comme effet secondaire, l'utilisation de structures). Mais oui: ils existent et ont un but.

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

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


12

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

Imaginez que le tableau est créé dans un bloc de mémoire alloué dans le tas non géré afin qu'un autre thread de code natif puisse accéder simultanément au tableau (peut-être du code à hautes performances faisant des calculs).

Imaginez que le code C # / VB écoute un flux du marché des changements de prix, ce code peut avoir à accéder à certains éléments du tableau (pour la sécurité quelle qu'elle soit), puis modifier certains 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 modifiables, 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 ou deux octets.

Hugo


3
Certes, mais dans ce cas, la raison de l'utilisation d'une structure est que cela est imposé à la conception de l'application par des contraintes externes (celles dues à l'utilisation du code natif). Tout ce que vous décrivez sur ces objets suggère qu'ils devraient clairement être des classes en C # ou VB.NET.
Jon Hanna du

6
Je ne sais pas pourquoi certaines personnes pensent que les choses devraient être des objets de classe. Si tous les emplacements de tableau sont remplis d'instances distinctes de références, l'utilisation d'un type de classe ajoutera douze ou vingt-quatre octets supplémentaires à la mémoire requise, et l'accès séquentiel sur un tableau de références d'objets de classe est susceptible d'être beaucoup plus lent que l'accès séquentiel sur un tableau de structures.
supercat

9

Quand quelque chose peut être muté, il acquiert un sentiment 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 qu'il Personest mutable, il est plus naturel de penser à changer la position d' Eric que de cloner Eric, de déplacer le clone et de 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 passer Eric (comme référence) pour les méthodes pour le modifier. Donner à une méthode un clone d'Eric va presque toujours surprendre. Quiconque veut muter Persondoit se rappeler de demander une référence Personou il fera la mauvaise chose.

Si vous rendez le type immuable, le problème disparaît; si je ne peux pas modifier eric, cela ne fait aucune différence pour moi si je reçois ericou un clone de eric. Plus généralement, un type est sûr à passer par valeur si tout son état observable est conservé dans des membres qui sont soit:

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

Si ces conditions sont remplies, un type de valeur mutable se comporte comme un type de référence car une copie superficielle permettra toujours au récepteur de modifier les données d'origine.

L'intuitivité d'un immuable Persondépend cependant de ce que vous essayez de faire. S'il Personne représente qu'un ensemble de données sur une personne, il n'y a rien d'intuitif à ce sujet; Personles variables représentent vraiment des valeurs abstraites , pas des objets. (Dans ce cas, il serait probablement plus approprié de le renommer PersonData.) Si Personmodélise en fait une personne elle - même, l'idée de créer en permanence et le déplacement des clones est stupide , même si vous avez évité l'écueil de penser que vous modifions l'original. Dans ce cas, il serait probablement plus naturel de simplement créer Personun type de référence (c'est-à-dire une classe).

Certes, comme la programmation fonctionnelle nous a appris qu'il est avantageux de tout rendre immuable (personne ne peut secrètement conserver une référence à lui ericet le muter), mais comme ce n'est pas idiomatique dans la POO, cela ne sera toujours pas intuitif pour quiconque travaillant avec votre code.


3
Votre point sur l' identité est bon; il peut être utile de noter que l'identité n'est pertinente que lorsqu'il existe plusieurs références à quelque chose. Si foodétient la seule référence à sa cible n'importe où dans l'univers, et que rien n'a capturé la valeur de hachage d'identité de cet objet, alors le champ de mutation foo.Xéquivaut sémantiquement à faire foopointer vers un nouvel objet qui est exactement comme celui auquel il faisait précédemment référence, mais avec Xen maintenant la valeur souhaitée. Avec les types de classe, il est généralement difficile de savoir s'il existe plusieurs références à quelque chose, mais avec les structures, c'est facile: elles ne le font pas.
supercat

1
Si Thingun type de classe mutable, un Thing[] va encapsulent des identités d'objet - si l' on veut qu'elle veuille ou non - à moins que l' on peut faire en sorte que pas Thingdans le tableau auquel toutes les références extérieures existent sera jamais muter. Si l'on ne veut pas que les éléments du tableau encapsulent l'identité, il faut généralement s'assurer qu'aucun élément auquel il contient des références ne sera jamais muté, ou qu'aucune référence extérieure n'existera jamais aux éléments qu'il détient [les approches hybrides peuvent également fonctionner ]. Aucune des deux approches n'est terriblement pratique. Si Thingest une structure, a Thing[]encapsule uniquement les valeurs.
supercat

Pour les objets, leur identité vient de leur emplacement. Les instances des types de référence ont leur identité grâce à leur emplacement dans la mémoire et vous ne faites circuler que leur identité (une référence), pas leurs données, tandis que les types de valeur ont leur identité à l'endroit externe où ils sont stockés. L'identité de votre type de valeur Eric provient uniquement de la variable où il est stocké. Si vous le faites circuler, il perdra son identité.
IllidanS4 veut que Monica revienne

6

Cela n'a rien à voir avec les structures (et pas avec C # non plus), mais en Java, vous pourriez avoir des problèmes avec des objets mutables lorsqu'ils sont par exemple des clés dans une carte de hachage. Si vous les modifiez après les avoir ajoutés à une carte et qu'elle modifie son code de hachage , de mauvaises choses peuvent se produire.


3
Cela est également vrai si vous utilisez une classe comme clé dans une carte.
Marc Gravell

6

Personnellement, quand je regarde le code, ce qui suit me semble assez maladroit:

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

plutôt que simplement

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

L'encapsulation des données est utile lors du passage d'une classe et vous voulez vous assurer que la valeur est modifiée de manière contrôlée. Cependant, lorsque vous avez des fonctions de définition et d'obtention publiques qui ne font guère plus que de définir la valeur de ce qui est transmis, en quoi cela représente-t-il une amélioration par rapport au simple passage d'une structure de données publique?

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

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


1
Droit au but! Les structures sont des unités d'organisation sans restrictions de contrôle d'accès! Malheureusement, C # les a rendus inutiles à cet effet!
ThunderGr

cela manque complètement le point car vos deux exemples montrent des structures mutables.
vidstige

C # les a rendus inutiles à cet effet parce que ce n'est pas le but des structures
Luiz Felipe

6

Il y a plusieurs problèmes avec l'exemple de M. Eric Lippert. Il est conçu pour illustrer le fait que les structures sont copiées et comment cela pourrait être un 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 un problème avec la structure ou la classe.

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

  2. Si vous avez une classe entourant une structure, vous appelleriez une méthode dans la classe pour muter la structure membre. C'est ce que je ferais comme une 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 plutôt qu'un problème avec la structure elle-même. Les structures sont censé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. . .


4
Il n'y a rien de mal à concevoir de petites structures opaques pour se comporter comme des objets de classe immuables; les directives MSDN sont raisonnables lorsque l'on essaie de créer quelque chose qui se comporte comme un objet . Les structures sont appropriées dans certains cas où l'on a besoin de choses légères qui se comportent comme des objets, et dans les cas où l'on a besoin d'un tas de variables collées ensemble avec du ruban adhésif. Pour une raison quelconque, cependant, beaucoup de gens ne réalisent pas que les structures ont deux usages distincts et que les directives appropriées pour l'une sont inappropriées pour l'autre.
supercat

5

Les données mutables présentent de nombreux avantages et inconvénients. L'inconvénient d'un million de dollars est l'aliasing. Si la même valeur est utilisée à plusieurs endroits, et que l'un d'eux la change, alors elle semblera avoir changé comme par magie aux autres endroits qui l'utilisent. Ceci est lié, mais non identique, aux conditions de course.

L'avantage d'un million de dollars est parfois la modularité. L'état mutable peut vous permettre de masquer les informations changeantes du code qui n'a pas besoin de les connaître.

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


les structures ne sont pas aliasées en c #. Chaque affectation de structure est une copie.
récursif

@recursive: Dans certains cas, c'est un avantage majeur des structures mutables, et celui qui me fait remettre en question la notion selon laquelle les structures ne devraient pas être mutables. Le fait que les compilateurs copient parfois implicitement des structures ne réduit pas l'utilité des structures mutables.
supercat

1

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

En utilisant l'exemple d'Eric, vous pouvez peut-être créer une deuxième instance de cet Eric, mais faire des ajustements, car c'est 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, sauf si vous prévoyez de 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 des structures), mais en ayant une structure immuable, cela empêche son utilisation de manière ennuyeuse.


Comme je le vois, une structure est en son cœur un tas de variables collées avec du ruban adhésif. Il est possible dans .NET pour une structure de faire semblant d'être autre chose qu'un tas de variables collées ensemble avec du ruban adhésif, et je suggérerais que lorsque c'est pratique un type qui va faire semblant d'être autre chose qu'un tas de variables collées ensemble avec du ruban adhésif doit se comporter comme un objet unifié (ce qui, pour une structure, impliquerait l'immuabilité), mais il est parfois utile de coller un tas de variables avec du ruban adhésif. Même dans le code de production, je considérerais qu'il vaut mieux avoir un type ...
supercat

... qui n'a clairement pas de sémantique au-delà de "chaque champ contient la dernière chose écrite", poussant toute la sémantique dans le code qui utilise la structure, plutôt que d'essayer d'avoir une structure en faire plus. Étant donné, par exemple, un Range<T>type avec des membres Minimumet des Maximumchamps de type Tet de code Range<double> myRange = foo.getRange();, toutes les garanties sur le contenu Minimumet le Maximumcontenu doivent provenir foo.GetRange();. Le fait d' Rangeêtre une structure de champ exposé indiquerait clairement que cela n'ajoutera aucun comportement en soi.
supercat
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.