Les types de valeur sont-ils immuables par définition?

Je lis souvent que les struct doivent être immuables – ne sont-elles pas par définition?

Pensez-vous que int soit immuable?

 int i = 0; i = i + 123; 

Cela semble correct – nous obtenons un nouvel int et le redonnons à i . Et ça?

 i++; 

D’accord, nous pouvons y voir un raccourci.

 i = i + 1; 

Qu’en est-il du Point struct ?

 Point p = new Point(1, 2); p.Offset(3, 4); 

Est-ce que cela change vraiment le point (1, 2) ? Ne devrions-nous pas penser à cela comme un raccourci pour ce qui suit avec Point.Offset() renvoyant un nouveau point?

 p = p.Offset(3, 4); 

Le fond de cette pensée est la suivante: comment un type de valeur sans identité peut-il être modifié? Vous devez l’examiner au moins deux fois pour déterminer si cela a changé. Mais comment pouvez-vous faire cela sans identité?

Je ne veux pas compliquer le raisonnement à ce sujet en considérant les parameters de ref et la boxe. Je suis également conscient que p = p.Offset(3, 4); exprime l’immuabilité bien mieux que p.Offset(3, 4); Est-ce que. Mais la question demeure: les types de valeur ne sont-ils pas immuables par définition?

METTRE À JOUR

Je pense qu’il y a au moins deux concepts en jeu: la mutabilité d’une variable ou d’un champ et la mutabilité de la valeur d’une variable.

 public class Foo { private Point point; private readonly Point readOnlyPoint; public Foo() { this.point = new Point(1, 2); this.readOnlyPoint = new Point(1, 2); } public void Bar() { this.point = new Point(1, 2); this.readOnlyPoint = new Point(1, 2); // Does not comstack. this.point.Offset(3, 4); // Is now (4, 6). this.readOnlyPoint.Offset(3, 4); // Is still (1, 2). } } 

Dans l’exemple, nous avons des champs – un mutable et un immuable. Etant donné qu’un champ de type valeur contient la valeur entière, un type de valeur stocké dans un champ immuable doit également être immuable. Je suis encore assez surpris du résultat – je ne pensais pas que le champ readonly restrait inchangé.

Les variables (à côté des constantes) sont toujours modifiables et n’impliquent donc aucune ressortingction quant à la mutabilité des types de valeur.


La réponse ne semble pas être aussi simple, je vais donc reformuler la question.

Compte tenu de ce qui suit.

 public struct Foo { public void DoStuff(whatEverArgumentsYouLike) { // Do what ever you like to do. } // Put in everything you like - fields, constants, methods, properties ... } 

Pouvez-vous donner une version complète de Foo et un exemple d’utilisation – pouvant inclure des parameters de ref et de la boxe – de sorte qu’il ne soit pas possible de réécrire toutes les occurrences de

 foo.DoStuff(whatEverArgumentsYouLike); 

avec

 foo = foo.DoStuff(whatEverArgumentsYouLike); 

Un object est immuable si son état ne change pas une fois que l’object a été créé.

Réponse courte: Non, les types de valeur ne sont pas immuables par définition. Les structures et les classes peuvent être mutables ou immuables. Les quatre combinaisons sont possibles. Si une structure ou une classe possède des champs publics non lus en lecture seule, des propriétés publiques avec des setters ou des méthodes définissant des champs privés, elle est modifiable, car vous pouvez modifier son état sans créer une nouvelle instance de ce type.


Réponse longue: Tout d’abord, la question de l’immutabilité ne s’applique qu’aux structures ou aux classes avec des champs ou des propriétés. Les types les plus élémentaires (nombres, chaînes et null) sont insortingnsèquement immuables car il n’y a rien (champ / propriété) à modifier à leur sujet. Un 5 est un 5 est un 5. Toute opération sur le 5 ne renvoie qu’une autre valeur immuable.

Vous pouvez créer des structures modifiables telles que System.Drawing.Point . X et Y ont des setters qui modifient les champs de la structure:

 Point p = new Point(0, 0); pX = 5; // we modify the struct through property setter X // still the same Point instance, but its state has changed // it's property X is now 5 

Certaines personnes semblent confondre l’immutablité avec le fait que les types de valeur sont passés par valeur (d’où leur nom) et non par référence.

 void Main() { Point p1 = new Point(0, 0); SetX(p1, 5); Console.WriteLine(p1.ToSsortingng()); } void SetX(Point p2, int value) { p2.X = value; } 

Dans ce cas, Console.WriteLine() écrit ” {X=0,Y=0} “. Ici p1 n’a pas été modifié car SetX() modifié p2 qui est une copie de p1 . Cela se produit parce que p1 est un type de valeur , pas parce qu’il est immuable (ce n’est pas le cas).

Pourquoi les types de valeur devraient- ils être immuables? Beaucoup de raisons … Voir cette question . La plupart du temps, c’est parce que les types de valeurs mutables conduisent à toutes sortes de bogues pas si évidents. Dans l’exemple ci-dessus, le programmeur aurait pu s’attendre à ce que p1 soit (5, 0) après avoir appelé SetX() . Ou imaginez un sorting par valeur qui peut changer par la suite. Ensuite, votre collection sortingée ne sera plus sortingée comme prévu. Il en va de même pour les dictionnaires et les hachages. Le fabuleux Eric Lippert ( blog ) a écrit toute une série sur l’immutabilité et pourquoi il croit que c’est l’avenir du C #. Voici un de ses exemples qui vous permet de “modifier” une variable en lecture seule.


UPDATE: votre exemple avec:

 this.readOnlyPoint.Offset(3, 4); // Is still (1, 2). 

C’est exactement ce à quoi Lippert a fait référence dans son article sur la modification de variables en lecture seule. Offset(3,4) fait modifié un Point , mais c’était une copie de readOnlyPoint , et il n’a jamais été atsortingbué à quoi que ce soit, il est donc perdu.

Et c’est pourquoi les types de valeur modifiables sont diaboliques: ils vous permettent de penser que vous modifiez quelque chose, alors que vous modifiez en fait parfois une copie, ce qui conduit à des bogues inattendus. Si Point était immuable, Offset() devrait renvoyer un nouveau Point et vous n’auriez pas pu l’affecter à readOnlyPoint . Et puis vous dites “Oh oui, c’est en lecture seule pour une raison. Pourquoi est-ce que j’essayais de le changer? Heureusement que le compilateur m’a arrêté maintenant.”


MISE À JOUR: À propos de votre demande reformulée … Je pense que je sais où vous voulez en venir. D’une certaine manière, vous pouvez “penser” que les structures sont immuables en interne , que modifier une structure revient à la remplacer par une copie modifiée. Ce pourrait même être ce que le CLR fait en mémoire interne, pour autant que je sache. (C’est ainsi que fonctionne la mémoire flash. Vous ne pouvez pas éditer que quelques octets, vous devez lire un bloc entier de kilo-octets dans la mémoire, modifier le nombre souhaité et réécrire l’intégralité du bloc.) Cependant, même s’ils étaient “immuables en interne” “, c’est un détail de mise en oeuvre et pour nous, développeurs, en tant qu’utilisateurs de structures (leur interface ou API, si vous préférez), elles peuvent être modifiées. Nous ne pouvons pas ignorer ce fait et “les considérer comme immuables”.

Dans un commentaire, vous avez dit “vous ne pouvez pas avoir de référence à la valeur d’un champ ou d’une variable”. Vous supposez que chaque variable de struct a une copie différente, de sorte que la modification d’une copie n’affecte pas les autres. Ceci n’est pas entièrement vrai. Les lignes marquées ci-dessous ne sont pas remplaçables si …

 interface IFoo { DoStuff(); } struct Foo : IFoo { /* ... */ } IFoo otherFoo = new Foo(); IFoo foo = otherFoo; foo.DoStuff(whatEverArgumentsYouLike); // line #1 foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2 

Les lignes 1 et 2 n’ont pas les mêmes résultats … Pourquoi? Parce que foo et otherFoo font référence à la même instance encadrée de Foo. Tout ce qui est changé dans foo à la ligne 1 est otherFoo dans otherFoo . La ligne 2 remplace foo par une nouvelle valeur et ne fait rien à otherFoo (en supposant que DoStuff() renvoie une nouvelle instance IFoo et ne modifie pas foo lui-même).

 Foo foo1 = new Foo(); // creates first instance Foo foo2 = foo1; // create a copy (2nd instance) IFoo foo3 = foo2; // no copy here! foo2 and foo3 refer to same instance 

La modification de foo1 n’affectera pas foo2 ou foo3 . La modification de foo2 reflètera dans foo3 , mais pas dans foo1 . La modification de foo3 reflètera dans foo2 mais pas dans foo1 .

Déroutant? Tenez-vous en aux types de valeurs immuables et éliminez l’envie de les modifier.


UPDATE: correction d’une faute de frappe dans le premier exemple de code

La mutabilité et les types de valeur sont deux choses distinctes.

La définition d’un type en tant que type de valeur indique que le moteur d’exécution copiera les valeurs au lieu d’une référence au moteur d’exécution. La mutabilité, en revanche, dépend de l’implémentation, et chaque classe peut l’implémenter à sa guise.

Vous pouvez écrire des structures mutables, mais il est recommandé de rendre les types de valeur immuables.

Par exemple, DateTime crée toujours de nouvelles instances pour toutes les opérations. Le point est modifiable et peut être changé.

Pour répondre à votre question: non, ils ne sont pas immuables par définition, cela dépend du cas s’ils doivent être mutables ou non. Par exemple, s’ils doivent servir de clés de dictionnaire, ils doivent être immuables.

Si vous prenez votre logique assez loin, alors tous les types sont immuables. Lorsque vous modifiez un type de référence, vous pouvez affirmer que vous écrivez réellement un nouvel object à la même adresse, plutôt que de modifier quoi que ce soit.

Vous pouvez également affirmer que tout est modifiable, dans n’importe quelle langue, car il arrive que la mémoire précédemment utilisée pour une chose soit remplacée par une autre.

Avec suffisamment d’abstractions et en ignorant suffisamment de fonctionnalités de langage, vous pouvez arriver à la conclusion que vous voulez.

Et cela manque le point. Selon les spécifications .NET, les types de valeur sont mutables. Vous pouvez le modifier.

 int i = 0; Console.WriteLine(i); // will print 0, so here, i is 0 ++i; Console.WriteLine(i); // will print 1, so here, i is 1 

mais c’est toujours le même i. La variable i n’est déclarée qu’une fois. Tout ce qui lui arrive après cette déclaration est une modification.

Dans quelque chose comme un langage fonctionnel avec des variables immuables, cela ne serait pas légal. Le ++ je ne serais pas possible. Une fois qu’une variable a été déclarée, elle a une valeur fixe.

Dans .NET, ce n’est pas le cas, rien ne m’empêche de modifier le i après sa déclaration.

Après avoir réfléchi un peu plus, voici un autre exemple qui pourrait être meilleur:

 struct S { public S(int i) { this.i = i == 43 ? 0 : i; } private int i; public void set(int i) { Console.WriteLine("Hello World"); this.i = i; } } void Foo { var s = new S(42); // Create an instance of S, internally storing the value 42 s.set(43); // What happens here? } 

Sur la dernière ligne, selon votre logique, nous pourrions dire que nous construisons réellement un nouvel object et écrasons l’ancien avec cette valeur. Mais ce n’est pas possible! Pour construire un nouvel object, le compilateur doit définir la variable i sur 42. Mais c’est privé! Il n’est accessible que via un constructeur défini par l’utilisateur, qui interdit explicitement la valeur 43 (en le définissant à 0 à la place), puis via notre méthode set , qui a un effet secondaire désagréable. Le compilateur n’a aucun moyen de créer un nouvel object avec les valeurs qu’il aime. Le seul moyen de définir si sur 43 est de modifier l’object actuel en appelant set() . Le compilateur ne peut pas simplement faire cela, car cela changerait le comportement du programme (il imprimerait sur la console)

Donc, pour que toutes les structures soient immuables, le compilateur devrait sortingcher et enfreindre les règles du langage. Et bien sûr, si nous sums prêts à enfreindre les règles, nous pouvons tout prouver. Je pourrais prouver que tous les entiers sont égaux également ou que la définition d’une nouvelle classe entraînera l’incendie de votre ordinateur. Tant que nous respectons les règles du langage, les structures sont mutables.

Je ne veux pas compliquer le raisonnement à ce sujet en considérant les parameters de ref et la boxe. Je suis également conscient que p = p.Offset(3, 4); exprime l’immuabilité bien mieux que p.Offset(3, 4); Est-ce que. Mais la question demeure: les types de valeur ne sont-ils pas immuables par définition?

Eh bien, alors vous ne travaillez pas vraiment dans le monde réel, n’est-ce pas? En pratique, la propension des types de valeur à faire des copies d’eux-mêmes lorsqu’ils se déplacent d’une fonction à l’autre va de pair avec l’immutabilité, mais ils ne sont pas immuables à moins que vous ne les rendiez immuables, car, comme vous l’avez souligné, vous pouvez utiliser des références à eux simplement. comme n’importe quoi d’autre.

Les types de valeur ne sont-ils pas immuables par définition?

Non, ils ne le sont pas: si vous regardez la structure System.Drawing.Point par exemple, elle a un setter ainsi qu’un getter sur sa propriété X

Cependant, il peut être vrai de dire que tous les types de valeur doivent être définis avec des API immuables.

Je pense que la confusion est que si vous avez un type de référence qui doit agir comme un type de valeur, c’est une bonne idée de le rendre immuable. L’une des principales différences entre les types de valeur et les types de référence est qu’une modification apscope à l’aide d’un nom sur un type de référence peut apparaître dans l’autre nom. Cela n’arrive pas avec les types de valeur:

 public class foo { public int x; } public struct bar { public int x; } public class MyClass { public static void Main() { foo a = new foo(); bar b = new bar(); ax = 1; bx = 1; foo a2 = a; bar b2 = b; ax = 2; bx = 2; Console.WriteLine( "a2.x == {0}", a2.x); Console.WriteLine( "b2.x == {0}", b2.x); } } 

Produit:

 a2.x == 2 b2.x == 1 

Maintenant, si vous avez un type pour lequel vous voudriez avoir une sémantique de valeur, mais ne voulez pas en faire un type de valeur – peut-être parce que le stockage qu’il nécessite est trop important ou autre, vous devriez considérer que l’immuabilité fait partie de la conception. Avec un type de référence immuable, toute modification apscope à une référence existante génère un nouvel object au lieu de modifier l’object existant. Vous obtenez ainsi le comportement du type de valeur, quelle que soit la valeur que vous détenez, elle ne peut pas être modifiée par un autre nom.

Bien entendu, la classe System.Ssortingng est un excellent exemple d’un tel comportement.

L’année dernière, j’ai écrit un article sur un blog concernant les problèmes que vous pouvez rencontrer en ne rendant pas les structures immuables.

Le post complet peut être lu ici

Voici un exemple de la façon dont les choses peuvent mal tourner:

 //Struct declaration: struct MyStruct { public int Value = 0; public void Update(int i) { Value = i; } } 

Exemple de code:

 MyStruct[] list = new MyStruct[5]; for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine(); for (int i=0;i<5;i++) list[i].Update(i+1); for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine(); 

La sortie de ce code est:

 0 0 0 0 0 1 2 3 4 5 

Faisons maintenant la même chose, mais substituons le tableau à une List<> générique List<> :

 List list = new List(new MyStruct[5]); for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine(); for (int i=0;i<5;i++) list[i].Update(i+1); for (int i=0;i<5;i++) Console.Write(list[i].Value + " "); Console.WriteLine(); 

La sortie est:

 0 0 0 0 0 0 0 0 0 0 

L'explication est très simple. Non, ce n'est pas de la boxe / unboxing ...

Lors de l'access aux éléments d'un tableau, le moteur d'exécution obtiendra les éléments directement, de sorte que la méthode Update () fonctionne sur l'élément de tableau lui-même. Cela signifie que les structures du tableau sont mises à jour.

Dans le deuxième exemple, nous avons utilisé une List<> générique List<> . Que se passe-t-il quand on accède à un élément spécifique? La propriété indexer est appelée méthode. Les types de valeur sont toujours copiés lorsqu'ils sont renvoyés par une méthode, c'est donc exactement ce qui se passe: la méthode d'indexation de la liste extrait la structure d'un tableau interne et la renvoie à l'appelant. Puisqu'il s'agit d'un type de valeur, une copie sera réalisée et la méthode Update () sera appelée sur la copie, ce qui n'aura bien sûr aucun effet sur les éléments d'origine de la liste.

En d'autres termes, assurez-vous toujours que vos structures sont immuables, car vous ne savez jamais quand une copie sera faite. La plupart du temps, c'est évident, mais dans certains cas, cela peut vraiment vous surprendre ...

Non ils ne sont pas. Exemple:

 Point p = new Point (3,4); Point p2 = p; p.moveTo (5,7); 

Dans cet exemple, moveTo() est une opération sur place . Cela change la structure qui se cache derrière la référence p . Vous pouvez voir cela en regardant à la page 2: sa position aura également changé. Avec des structures immuables, moveTo() devrait retourner une nouvelle structure:

 p = p.moveTo (5,7); 

Désormais, Point est immuable et lorsque vous créez une référence à un endroit quelconque de votre code, vous n’aurez aucune surprise. Regardons i :

 int i = 5; int j = i; i = 1; 

Ceci est différent. i ne i pas immuable, 5 est. Et la deuxième affectation ne copie pas une référence à la structure qui contient i mais copie le contenu de i . En coulisse, il se passe quelque chose de complètement différent: vous obtenez une copie complète de la variable au lieu d’une copie de l’adresse en mémoire (la référence).

Un équivalent avec des objects serait le constructeur de copie:

 Point p = new Point (3,4); Point p2 = new Point (p); 

Ici, la structure interne de p est copiée dans un nouvel object / structure et p2 en contiendra la référence. Mais comme il s’agit d’une opération assez coûteuse (contrairement à l’affectation d’entiers ci-dessus), la plupart des langages de programmation font la distinction.

À mesure que les ordinateurs deviennent plus puissants et disposent de plus de mémoire, cette distinction disparaîtra car elle causerait une quantité énorme de bugs et de problèmes. Dans la prochaine génération, il n’y aura plus que des objects immuables, toute opération sera protégée par une transaction et même un int sera un object complet. Tout comme la collecte des ordures ménagères, ce sera un grand pas en avant dans la stabilité du programme. Cela causera beaucoup de chagrin au cours des premières années, mais cela permettra d’écrire un logiciel fiable. Aujourd’hui, les ordinateurs ne sont tout simplement pas assez rapides pour cela.

Non, les types de valeur ne sont pas immuables par définition.

D’abord, j’aurais mieux fait de poser la question “Les types de valeur se comportent-ils comme des types immuables?” au lieu de demander si elles sont immuables – je suppose que cela a causé beaucoup de confusion.

 struct MutableStruct { private int state; public MutableStruct(int state) { this.state = state; } public void ChangeState() { this.state++; } } struct ImmutableStruct { private readonly int state; public MutableStruct(int state) { this.state = state; } public ImmutableStruct ChangeState() { return new ImmutableStruct(this.state + 1); } } 

[À suivre…]

Pour définir si un type est mutable ou immuable, il faut définir à quoi ce “type” fait référence. Lorsqu’un emplacement de stockage de type référence est déclaré, la déclaration alloue simplement de l’espace pour contenir une référence à un object stocké ailleurs. la déclaration ne crée pas l’object réel en question. Néanmoins, dans la plupart des contextes où l’on parle de types de référence particuliers, on ne parlera pas d’un emplacement de stockage contenant une référence , mais plutôt de l’object identifié par cette référence . Le fait que l’on puisse écrire dans un emplacement de stockage contenant une référence à un object n’implique en aucune manière que l’object lui-même soit mutable.

En revanche, lorsqu’un emplacement de stockage de type valeur est déclaré, le système allouera à cet emplacement de stockage des emplacements de stockage nesteds pour chaque champ public ou privé détenu par ce type de valeur. Tout ce qui concerne le type de valeur est stocké dans cet emplacement de stockage. Si on définit une variable foo de type Point et ses deux champs, X et Y , tenez respectivement 3 et 6. Si l’on définit “l’instance” de Point in foo comme étant la paire de champs , cette instance sera mutable si et seulement si foo est mutable. If one defines an instance of Point as being the values held in those fields (eg “3,6”), then such an instance is by definition immutable, since changing one of those fields would cause Point to hold a different instance.

I think it is more helpful to think of a value type “instance” as being the fields, rather than the values they hold. By that definition, any value type stored in a mutable storage location, and for which any non-default value exists, will always be mutable, regardless of how it is declared. A statement MyPoint = new Point(5,8) constructs a new instance of Point , with fields X=5 and Y=8 , and then mutates MyPoint by replacing the values in its fields with those of the newly-created Point . Even if a struct provides no way to modify any of its fields outside its constructor, there is no way a struct type can protect an instance from having all of its fields overwritten with the content of another instance.

Incidentally, a simple example where a mutable struct can achieve semantics not achievable via other means: Assuming myPoints[] is a single-element array which is accessible to multiple threads, have twenty threads simultaneously execute the code:

 Threading.Interlocked.Increment(myPoints[0].X); 

If myPoints[0].X starts out equal to zero and twenty threads perform the above code, whether simultaneously or not, myPoints[0].X will equal twenty. If one were to attempt to mimic the above code with:

 myPoints[0] = new Point(myPoints[0].X + 1, myPoints[0].Y); 

then if any thread read myPoints[0].X between the time another thread read it and wrote back the revised value, the results of the increment would be lost (with the consequence that myPoints[0].X could arbitrarily end up with any value between 1 and 20.

Objects/Structs are immutable when they are passed into a function in such a way as the data cannot be changed, and the returned struct is a new struct. The classic example is

Ssortingng s = "abc";

s.toLower();

if the toLower function is written so they a new ssortingng is returned that replaces “s”, it’s immutable, but if the function goes letter by letter replacing the letter inside “s” and never declaring a “new Ssortingng”, it is mutable.