Les types de valeurs sont-ils immuables par définition?
j'ai souvent lu que struct
S devrait être immuable - ne sont-ils pas par définition?
considérez-vous que int
est immuable?
int i = 0;
i = i + 123;
semble correct - nous obtenons un nouveau int
et l'assignons de nouveau à i
. Quoi à ce sujet?
i++;
d'accord, on peut y voir un raccourci.
i = i + 1;
et le struct
Point
?
Point p = new Point(1, 2);
p.Offset(3, 4);
est-ce que cela mute vraiment le point (1, 2)
? Ne devrions-nous pas y voir un raccourci pour les suivants avec Point.Offset()
qui renvoie un nouveau point?
p = p.Offset(3, 4);
le fond de cette pensée est ceci - comment un type de valeur sans identité peut-il être mutable? Vous devez le regarder au moins deux fois pour déterminer s'il a changé. Mais comment pouvez-vous faire cela sans identité?
je ne veux pas compliquer raisonner à ce sujet en considérant les paramètres ref
et la boxe. Je suis également conscient que p = p.Offset(3, 4);
exprime l'immuabilité beaucoup mieux que p.Offset(3, 4);
ne le fait. Mais la question demeure : les types de valeurs ne sont-ils pas immuables par définition?
mise à JOUR
je pense qu'il y a au moins deux concepts - le changement 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 compile.
this.point.Offset(3, 4); // Is now (4, 6).
this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
}
}
dans l'exemple nous devons fields - un mutable et un immuable. Parce qu'un champ de type de valeur contient la valeur entière, un type de valeur stocké dans un champ immuable doit être immuable, aussi. Je suis toujours très surpris par le résultat - je n'ai pas exspect la readonly champ inchangées.
les Variables (en dehors des constantes) sont tout de même mutables, donc elles n'impliquent aucune restriction sur la mutabilité des types de valeurs.
la réponse ne semble pas si simple, alors je vais reformuler la question.
étant donné 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 - qui peut inclure ref
paramètres et la boxe-de sorte qu'il n'est pas possible de réécrire toutes les occurences de
foo.DoStuff(whatEverArgumentsYouLike);
avec
foo = foo.DoStuff(whatEverArgumentsYouLike);
12 réponses
Un objet est immuable si son état ne pas changer une fois que l'objet a été créé.
brève réponse: non, les types de valeurs ne sont pas immuables par définition. les deux structures et classes peuvent être soit mutables, soit immuables. les quatre combinaisons sont possibles. Si une structure ou une classe a des champs publics non-readonly, des propriétés publiques avec setters, ou des méthodes qui fixent des champs privés, elle est mutable parce que vous pouvez changer son état sans créer une nouvelle instance de ce type.
longue réponse: tout d'abord, la question de l'immuabilité ne s'applique qu'aux structures ou classes avec des champs ou des propriétés. Les types les plus basiques (nombres, chaînes, et null) sont intrinsèquement immuables parce qu'il n'y a rien (champ/propriété) à changer à leur sujet. 5 à 5 à 5. Toute opération sur le 5 ne renvoie qu'une autre valeur immuable.
You peut créer des structures mutables telles que System.Drawing.Point
. Les deux X
et Y
ont des setters qui modifient les champs de la structure:
Point p = new Point(0, 0);
p.X = 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 immutabilité 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.ToString());
}
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é parce que SetX()
modifié p2
qui est un copie de p1
. Cela se produit parce que p1
est un type de valeur , non pas parce qu'il est immuable (il n'est pas).
pourquoi les types de valeurs 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 peut avoir on s'attend à ce que p1
soit (5, 0)
après avoir appelé SetX()
. Ou imaginez le tri par une valeur qui peut changer plus tard. Alors votre collection triée ne sera plus triée comme prévu. Il en va de même pour les dictionnaires et les hachures. Le fabuleux Eric Lippert ( blog ) a écrit une série entière sur l'immuabilité et pourquoi il croit que c'est l'avenir de C#. voici un de ses exemples qui vous permet de "modifier" en lecture seule variable.
mise à JOUR: votre exemple:
this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
est exactement ce à quoi Lippert fait référence dans son billet sur la modification des variables en lecture seule. Offset(3,4)
a en fait modifié un Point
, mais c'était une" copie de readOnlyPoint
, et il n'a jamais été attribué à quoi que ce soit, il est donc perdu.
et que c'est pourquoi les types de valeurs mutables sont mauvais: ils vous laissent penser vous modifiez quelque chose, alors que parfois vous modifiez réellement une copie, ce qui conduit à des bugs inattendus. Si Point
était immuable, Offset()
aurait à retourner une nouvelle Point
, et vous n'auriez pas été en mesure d'attribuer à readOnlyPoint
. Et puis on dit " Oh bon, c'est en lecture seule pour une raison. Pourquoi suis-je en essayant de le changer? Heureusement que le compilateur m'a arrêté."
mise à jour: à propos de votre requête reformulée... Je pense que je sais ce que vous allez obtenir. D'une certaine manière, vous pouvez "penser" aux structures comme étant intérieurement immuable, que modifier une structure est la même chose que la remplacer par une copie modifiée. C'est peut-être même ce que le CLR fait en interne dans la mémoire, pour ce que je sais. (C'est comme ça que fonctionne la mémoire flash. Vous ne pouvez pas éditer juste quelques octets, vous devez lire un bloc entier de kilooctets dans la mémoire, modifier le peu que vous voulez, et écrire le bloc entier en arrière.) Cependant, même s'ils étaient "intérieurement immuables", c'est-à-dire un détail d'implémentation et pour les développeurs américains en tant qu'utilisateurs de structures (leur interface ou API, si vous voulez), ils peuvent être changés. Nous ne pouvons pas ignorer ce fait et "les considérer comme immuables".
dans un commentaire vous avez dit"vous ne pouvez pas avoir une référence à la valeur du champ ou de la variable". Vous êtes en supposant que chaque la variable struct a une copie différente, de sorte que la modification d'une copie n'affecte pas les autres. Ce n'est pas tout à fait vrai. Les lignes indiqué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
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 boxée de Foo. Tout ce qui est modifié dans foo
dans la ligne #1 se reflète dans otherFoo
. La ligne 2 remplace foo
par un nouveau valeur et ne fait rien pour otherFoo
(en supposant que DoStuff()
renvoie une nouvelle IFoo
exemple et ne pas modifier 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
modifiant foo1
n'affectera pas foo2
ou foo3
. La modification de foo2
se reflétera dans foo3
, mais pas dans foo1
. La modification de foo3
se reflétera dans foo2
mais pas dans foo1
.
déroutant? Bâton de immuable les types de valeur et vous éliminez l'envie de modifier l'un d'eux.
mise à JOUR: correction d'une faute dans le premier exemple de code
mutabilité et types de valeur sont deux choses distinctes.
définit un type comme un type de valeur, indique que l'exécution copiera les valeurs au lieu d'une référence à l'exécution. La mutabilité, d'un autre côté, dépend de l'implémentation, et chaque classe peut l'implémenter comme elle le veut.
vous pouvez écrire des structures qui sont mutables, mais c'est une bonne pratique de rendre les types de valeur immuables.
par exemple DateTime crée toujours de nouvelles instances lors d'une opération. Point est mutable et peut être changé.
pour répondre à votre question: non, ils ne sont pas immuables par définition, cela dépend du cas si elles devraient être mutables ou non. Par exemple, s'ils doivent servir de clés de dictionnaires, ils doivent être immuables.
si vous allez assez loin dans votre logique, alors tous les types sont immuables. Quand vous modifiez un type de référence, vous pourriez argumenter que vous écrivez vraiment un nouvel objet à la même adresse, plutôt que de modifier quoi que ce soit.
ou vous pourriez argumenter que tout est mutable, dans n'importe quelle langue, parce que de temps en temps la mémoire qui avait été utilisée auparavant pour une chose, sera écrasée par une autre.
avec assez abstractions, et en ignorant assez de traits de langage, vous pouvez arriver à n'importe quelle conclusion que vous aimez.
et ça manque le but. Selon la spécification .NET, les types de valeurs 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 seule fois. Tout ce qui lui arrive après cette déclaration est une modification.
Dans quelque chose comme un langage fonctionnel avec immuables variables, ce ne serait pas légal. Le ++je ne serait pas possible. Une fois qu'une variable a été déclarée, il a une valeur fixe.
dans .NET, ce n'est pas le cas, il n'y a rien pour m'empêcher de modifier le i
après qu'il a été déclaré.
après y 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 avons fait construisez un nouvel objet, et écrasez l'ancien avec cette valeur.
Mais ce n'est pas possible! Pour construire un nouvel objet, le compilateur doit définir la variable i
à 42. Mais c'est privé! Il n'est accessible que par un constructeur défini par l'utilisateur, qui rejette explicitement la valeur 43 (La mettant à 0 à la place), et ensuite par notre méthode set
, qui a un effet secondaire désagréable. Le compilateur n'a aucun moyen de juste créer un nouvel objet avec les valeurs qu'il comme. La seule façon de régler s.i
à 43 est de modifiant l'objet courant en appelant set()
. Le compilateur ne peut pas simplement faire cela, parce qu'il modifierait le comportement du programme (il imprimerait sur la console)
donc pour que toutes les structures soient immuables, le compilateur devrait tricher et enfreindre les règles du langage. Et bien sûr, si nous sommes prêts à enfreindre les règles, nous pouvons tout prouver. Je pourrais prouver que tout les entiers sont égaux aussi, ou que la définition d'une nouvelle classe causera le feu à votre ordinateur. Tant que nous restons dans les règles du langage, les structures sont mutables.
je ne veux pas compliquer le raisonnement à ce sujet en considérant
ref
les paramètres et la boxe. Je suis également conscient quep = p.Offset(3, 4);
exprime immutabilité bien mieux quep.Offset(3, 4);
ne. Mais l' la question demeure - les types de valeur ne sont pas immuable par définition?
alors vous n'opérez pas vraiment dans le monde réel, n'est-ce pas? Dans la pratique, la propension des types de valeur à se faire des copies se déplacer entre les fonctions correspond bien à l'immutabilité, mais elles ne sont pas réellement immuables sauf si vous les rendez immuables, puisque, comme vous l'avez souligné, vous pouvez utiliser des références à elles comme n'importe quoi d'autre.
les types de valeurs 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 devraient être définis avec des API immuables.
je pense que la confusion est que si vous avez un type de référence qui devrait agir comme un type de valeur c'est une bonne idée de le rendre immuable. Une des principales différences entre les types de valeurs et les types de référence est qu'un changement effectué à travers un nom sur un type ref peut apparaître dans l'autre nom. Cela n'arrive pas avec les types de valeurs:
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();
a.x = 1;
b.x = 1;
foo a2 = a;
bar b2 = b;
a.x = 2;
b.x = 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 que vous aimeriez avoir valeur sémantique, mais ne voulez pas réellement en faire un type de valeur - peut-être parce que le stockage qu'il nécessite est trop ou quoi que ce soit d'autre, vous devriez considérer que l'immuabilité fait partie de la conception. Avec un type ref immuable, toute modification apportée à une référence existante produit un nouvel objet au lieu de changer l'objet existant, de sorte que vous obtenez le comportement du type de valeur que quelle que soit la valeur que vous détenez ne peut pas être changée par un autre nom.
bien sûr le système.La classe String en est un bon exemple de tels comportements.
L'année dernière, j'ai écrit un billet de blog concernant les problèmes que vous pouvez rencontrer en ne faisant pas de structures immuable.
le message complet peut être lu ici
Ceci est un exemple de comment les choses peuvent aller horriblement mal:
//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
maintenant faisons la même chose, mais substituons la tableau pour un List<>
Générique:
List<MyStruct> list = new List<MyStruct>(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, il n'est pas boxing/unboxing...
lors de l'accès à des éléments d'un tableau, le runtime obtiendra les éléments du tableau directement, de sorte que la méthode Update() Fonctionne sur l'élément du tableau lui-même. Cela signifie que les structures elles-mêmes dans le tableau sont mises à jour.
Dans le deuxième exemple, nous avons utilisé un générique List<>
. Que se passe-t-il lorsque nous accédons à un élément spécifique? Eh bien, l'indexeur propriété est appelée, ce qui est une méthode. Les types de valeurs sont toujours copiés lorsqu'ils sont retournés par une méthode, donc c'est exactement ce qui arrive: la méthode indexer de la liste récupère la structure d'un tableau interne et la renvoie à l'appelant. Parce qu'il s'agit d'un type de valeur, une copie sera faite, et la méthode Update() sera appelée sur la copie, ce qui bien sûr n'a aucun effet sur les éléments originaux de la liste.
En d'autres termes, assurez-vous toujours que vos structures sont immuables, parce que vous n'êtes jamais sûr quand une copie sera faite. La plupart du temps, c'est évident, mais dans certains cas, il peut vraiment vous surprendre...
Non, ils ne le sont pas. Exemple:
Point p = new Point (3,4);
Point p2 = p;
p.moveTo (5,7);
dans cet exemple, moveTo()
est une opération en place . Il modifie la structure qui se cache derrière la référence p
. Vous pouvez le voir en regardant p2
: sa position aura également changé. Avec des structures immuables, moveTo()
devrait retourner une nouvelle structure:
p = p.moveTo (5,7);
maintenant, Point
est immuable et quand vous créez une référence à n'importe où dans votre code, vous n'aurez pas de surprises. Regardons i
:
int i = 5;
int j = i;
i = 1;
c'est différent. i
n'est pas immuable, 5
est. Et la seconde cession ne copie pas une référence à la structure qui contient i
mais copie le contenu de i
. Donc dans les coulisses, quelque chose de complètement différent se produit: vous obtenez une copie complète de la variable au lieu de seulement une copie de l'adresse dans de mémoire (la référence).
un équivalent avec des objets 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 objet/structure et p2
contiendra la référence à elle. Mais c'est une opération assez coûteuse (à la différence de la tâche entière ci-dessus) qui est la raison pour laquelle la plupart des langages de programmation font la distinction.
comme les ordinateurs deviennent plus puissants et obtenez plus de mémoire, cette distinction va disparaître parce qu'il provoque une énorme quantité de bogues et de problèmes. Dans la génération suivante, il n'y aura que des objets immuables, toute opération sera protégée par une transaction et même un int
sera un objet complet. Tout comme la collecte des ordures, ce sera un grand pas en avant dans la stabilité du programme, causer beaucoup de chagrin dans les premières années, mais il permettra d'écrire des logiciels fiables. Aujourd'hui, les ordinateurs ne sont pas assez rapides pour ce.
non, les types de valeur sont et non immuables par définition.
tout d'abord, j'aurais mieux fait de poser la question "Est-ce que les types de valeurs se comportent 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 de référence est déclarée, la déclaration simplement alloue de l'espace pour contenir une référence à un objet stocké ailleurs; la déclaration ne crée pas l'objet 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 qui contient une référence , mais plutôt l'objet identifié par la référence . Le fait qu'on puisse écrire à un endroit de stockage contenant une référence à un objet n'implique en aucune façon que l'objet lui-même est mutable.
en revanche, lorsqu'un emplacement de stockage de type valeur est déclaré, le système attribuera à l'intérieur de cet emplacement de stockage des emplacements de stockage imbriqués pour chaque champ public ou privé détenu par ce type de valeur. Tout sur le type de valeur est détenu dans ce lieu de stockage. Si l'on définit une variable foo
de type Point
et ses deux champs, X
et Y
, retenez 3 et 6 respectivement. Si l'on définit le "instance" de Point
dans foo
comme étant la paire de champs , cette instance sera mutable si et seulement si foo
est mutable. Si l'on définit une instance de Point
comme étant les valeurs contenues dans ces champs (par exemple" 3,6"), alors une telle instance est par définition immuable, puisque changer l'un de ces champs ferait en sorte que Point
tienne une instance différente.
je pense qu'il est plus utile de penser à un type de valeur" instance " comme étant les champs, plutôt que les valeurs qu'ils contiennent. Selon cette définition, tout type de valeur stocké dans un lieu de stockage mutable, et pour lequel il existe une valeur non par défaut, sera toujours mutable, quelle que soit la façon dont il est déclaré. Un la déclaration MyPoint = new Point(5,8)
construit une nouvelle instance de Point
, avec les champs X=5
et Y=8
, et Mute ensuite MyPoint
en remplaçant les valeurs dans ses champs par celles de la nouvelle création Point
. Même si une struct ne fournit aucun moyen de modifier un de ses champs en dehors de son constructeur, il n'y a aucun moyen qu'un type de struct puisse protéger une instance d'avoir tous ses champs réécrits avec le contenu d'une autre instance.
soit dit en passant, un exemple simple où une structure mutable peut réaliser une sémantique non réalisable par d'autres moyens: en supposant que myPoints[]
est un tableau à un seul élément qui est accessible à plusieurs threads, avoir vingt threads simultanément exécuter le code:
Threading.Interlocked.Increment(myPoints[0].X);
si myPoints[0].X
commence égal à zéro et vingt threads effectuer le code ci-dessus, que ce soit simultanément ou non, myPoints[0].X
sera égal vingt. Si l'on tentait d'imiter le code ci-dessus avec:
myPoints[0] = new Point(myPoints[0].X + 1, myPoints[0].Y);
alors si n'importe quel fil a lu myPoints[0].X
entre le temps qu'un autre fil l'a lu et a écrit en arrière la valeur révisée, les résultats de l'incrément seraient perdus (avec la conséquence que myPoints[0].X
pourrait arbitrairement finir vers le haut avec n'importe quelle valeur entre 1 et 20.
objets / structures sont immuables quand ils sont passés dans une fonction de telle manière que les données ne peuvent pas être changées, et la structure retournée est une structure new
. L'exemple classique est
String s = "abc";
s.toLower();
si la fonction toLower
est écrite de façon à ce qu'une nouvelle chaîne de caractères soit retournée qui remplace "s", c'est immuable, mais si la fonction va lettre par lettre en remplaçant la lettre à l'intérieur de "s" et ne jamais déclarer une "nouvelle chaîne", il est mutable.