J'ai récemment essayé de créer une propriété pour un Vector2
champ, juste pour réaliser que cela ne fonctionne pas comme prévu.
public Vector2 Position { get; set; }
cela m'empêche de changer les valeurs de ses membres (X
& Y
)
En recherchant des informations à ce sujet, j'ai lu que la création d'une propriété à un Vector2
struct renvoie uniquement une copie de l'objet d'origine et non une référence.
En tant que développeur Java, cela m'embrouille.
Quand les objets en C # sont-ils passés par valeur et quand sont-ils passés par référence?
Tous les objets struct sont-ils passés par valeur?
Il est important de réaliser que tout en C # est passé par valeur , sauf si vous spécifiez ref
ou out
dans la signature.
Ce qui différencie les types de valeur (et donc struct
s) des types de référence, c'est qu'un type de valeur est accessible directement, tandis qu'un type de référence est accessible via sa référence. Si vous passez un type de référence dans une méthode, sa référence, pas la valeur elle-même, est passée par valeur.
Pour illustrer cela, imaginez que nous avons une classePointClass
et une structPointStruct
, définies de façon analogue (en omettant les détails non pertinents):
struct PointStruct { public int x, y; }
class PointClass { public int x, y; }
Et nous avons une méthode SomeMethod
qui prend ces deux types par valeur:
static void ExampleMethod(PointClass apc, PointStruct aps) { … }
Si nous créons maintenant deux objets et appelons la méthode:
var pc = new PointClass(1, 1);
var ps = new PointStruct(1, 1);
ExampleMethod(pc, ps);
… Nous pouvons visualiser cela avec le schéma suivant:
Puisque pc
est une référence, elle ne contient pas la valeur en elle-même; il fait plutôt référence à une valeur (sans nom) ailleurs dans la mémoire. Ceci est visualisé par la bordure en pointillés et la flèche.
Mais: pour pc
et ps
, la variable réelle est copiée lors de l'appel de la méthode.
Que se passe-t-il si ExampleMethod
réattribue les variables d'argument en interne? Allons vérifier:
static void ExampleMethod(PointClass apc, PointStruct aps); {
apc = new PointClass(2, 2);
aps = new PointStruct(2, 2);
}
Sortie de pc
et ps
après avoir appelé la méthode:
pc: {x: 1, y: 1}
ps: {x: 1, y: 1}
→ ExampleMethod
a modifié une copie des valeurs et les valeurs d'origine ne sont pas affectées.
C'est, fondamentalement, ce que signifie "passer par la valeur".
Il existe toujours une différence entre les types de référence et de valeur, et cela entre en jeu lorsque vous modifiez membres de la valeur, pas la variable elle-même. C'est la partie qui déclenche les gens lorsqu'ils sont confrontés au fait que les types de référence sont passés par valeur. Considérons un ExampleMethod
différent.
static void ExampleMethod(PointClass apc, PointStruct aps) {
apc.x = 2;
aps.x = 2;
}
Nous observons maintenant le résultat suivant après avoir appelé la méthode:
pc: {x: 2, y: 1}
ps: {x: 1, y: 1}
→ L'objet de référence a été modifié, contrairement à l'objet de valeur. Le diagramme ci-dessus montre pourquoi: pour l'objet de référence, même si pc
a été copié, la valeur réelle que la référence pc
et apc
reste identique, et nous pouvons la modifier via apc
. Quant à ps
, nous avons copié la valeur réelle elle-même dans aps
; ExampleMethod
ne peut pas toucher la valeur d'origine.
Un struct
est un type de valeur, il est donc toujours transmis en tant que valeur.
Une valeur peut être soit un type de référence (objet), soit un type de valeur (struct). Ce qui se passe est toujours une valeur; pour un type de référence, vous lui transmettez la valeur de la référence, pour un type de valeur, vous passez la valeur elle-même.
Le terme par référence est utilisé lorsque vous utilisez les mots clés ref
ou out
pour passer un paramètre. Ensuite, vous transmettez une référence à la variable qui contient la valeur au lieu de transmettre la valeur. Normalement, un paramètre est toujours transmis par valeur.
Avant-propos: C # tout en étant géré a toujours les idiomes de mémoire de base créés par C. La mémoire peut être raisonnablement considérée comme un tableau géant où l'index dans le tableau est étiqueté "adresse de mémoire". Un pointeur est un index numérique de ce tableau alias une mémoire adresse. Les valeurs de ce tableau peuvent être des données ou un pointeur vers une autre adresse mémoire. Un const pointer est une valeur stockée dans ce tableau à un indice qui ne peut pas changer. Une adresse mémoire existe intrinsèquement et ne peut jamais changer, cependant la valeur qui se trouve à cette adresse peut toujours changer si elle est pas const.
Un type de classe/référence (qui inclut une chaîne) est transmis par une référence de pointeur const. La mutation affectera toutes les utilisations de cette instance. Vous ne pouvez pas modifier l'adresse de l'objet. Si vous tentez de modifier l'adresse avec une affectation ou new
, vous créerez en fait une variable locale qui partage le même nom que le paramètre dans la portée actuelle.
Les primitives/ValueTypes/structs (les chaînes ne sont pas non plus même si elles se présentent comme des faux) sont complètement copiées lorsqu'elles sont renvoyées d'une méthode, d'une propriété ou reçues en tant que paramètre. La mutation d'une structure ne sera jamais partagée. Si une structure contient un membre de classe, ce qui est copié est la référence du pointeur. Cet objet membre serait modifiable.
Les primitives ne sont jamais mutables. Vous ne pouvez pas muter de 1 à 2, vous pouvez muter l'adresse mémoire qui se réfère actuellement à 1 à l'adresse mémoire de 2 dans la portée actuelle.
Nécessite l'utilisation de mots clés out
ou ref
.
ref
vous permettra de modifier le pointeur d'un objet new
ou d'affecter un objet existant. ref
vous permettra également de passer une primitive/ValueType/struct par son pointeur mémoire pour éviter de copier l'objet. Cela vous permettrait également de remplacer le pointeur par une primitive différente si vous lui affectiez.
out
est sémantiquement identique à ref
avec une différence mineure. ref
les paramètres doivent être initialisés où out
les paramètres peuvent être non initialisés car ils doivent être initialisés dans la méthode qui accepte le paramètre. Ceci est généralement montré dans les méthodes TryParse
et élimine le besoin d'avoir int x = 0; int.TryParse("5", out x)
lorsque la valeur initiale de x ne servirait à rien.
Les types de données .NET sont divisés en types valeur et référence. Les types de valeurs incluent int
, byte
et struct
s. Les types de référence incluent string
et les classes.
les structures sont appropriées au lieu de classes lorsqu'elles contiennent juste un ou deux types de valeurs (bien que même là, vous pouvez avoir des effets secondaires involontaires).
Ainsi, les structures sont en effet transmises par valeur et ce que vous voyez est attendu.
Juste pour illustrer les différents effets du passage de struct vs classe à travers des méthodes:
(note: testé dans LINQPad 4 )
/// via http://stackoverflow.com/questions/9251608/are-structs-pass-by-value
void Main() {
// just confirming with delegates
Action<StructTransport> delegateTryUpdateValueType = (t) => {
t.i += 10;
t.s += ", appended delegate";
};
Action<ClassTransport> delegateTryUpdateRefType = (t) => {
t.i += 10;
t.s += ", appended delegate";
};
// initial state
var structObject = new StructTransport { i = 1, s = "one" };
var classObject = new ClassTransport { i = 2, s = "two" };
structObject.Dump("Value Type - initial");
classObject.Dump("Reference Type - initial");
// make some changes!
delegateTryUpdateValueType(structObject);
delegateTryUpdateRefType(classObject);
structObject.Dump("Value Type - after delegate");
classObject.Dump("Reference Type - after delegate");
methodTryUpdateValueType(structObject);
methodTryUpdateRefType(classObject);
structObject.Dump("Value Type - after method");
classObject.Dump("Reference Type - after method");
methodTryUpdateValueTypePassByRef(ref structObject);
methodTryUpdateRefTypePassByRef(ref classObject);
structObject.Dump("Value Type - after method passed-by-ref");
classObject.Dump("Reference Type - after method passed-by-ref");
}
// the constructs
public struct StructTransport {
public int i { get; set; }
public string s { get; set; }
}
public class ClassTransport {
public int i { get; set; }
public string s { get; set; }
}
// the methods
public void methodTryUpdateValueType(StructTransport t) {
t.i += 100;
t.s += ", appended method";
}
public void methodTryUpdateRefType(ClassTransport t) {
t.i += 100;
t.s += ", appended method";
}
public void methodTryUpdateValueTypePassByRef(ref StructTransport t) {
t.i += 1000;
t.s += ", appended method by ref";
}
public void methodTryUpdateRefTypePassByRef(ref ClassTransport t) {
t.i += 1000;
t.s += ", appended method by ref";
}
(depuis le vidage LINQPad)
Value Type - initial
StructTransport
UserQuery+StructTransport
i 1
s one
Reference Type - initial
ClassTransport
UserQuery+ClassTransport
i 2
s two
//------------------------
Value Type - after delegate
StructTransport
UserQuery+StructTransport
i 1
s one
Reference Type - after delegate
ClassTransport
UserQuery+ClassTransport
i 12
s two, appended delegate
//------------------------
Value Type - after method
StructTransport
UserQuery+StructTransport
i 1
s one
Reference Type - after method
ClassTransport
UserQuery+ClassTransport
i 112
s two, appended delegate, appended method
//------------------------
Value Type - after method passed-by-ref
StructTransport
UserQuery+StructTransport
i 1001
s one, appended method by ref
Reference Type - after method passed-by-ref
ClassTransport
UserQuery+ClassTransport
i 1112
s two, appended delegate, appended method, appended method by ref
Le problème est que le getter retourne une copie de Vector2
. Si vous changez les coordonnées comme ceci
obj.Position.X = x;
obj.Position.Y = y;
Vous ne modifiez que les coordonnées de cette copie éphémère.
Faites cela à la place
obj.Position = new Vector2(x, y);
Cela n'a rien à voir avec la valeur ou la référence. Value2
est un type de valeur et get
renvoie cette valeur. Si le vecteur avait un type de référence (classe), get
retournerait cette référence. return
renvoie les valeurs par valeur. Si nous avons un type de référence, ces références sont les valeurs et sont renvoyées.
Les objets sont passés par référence et les structures par valeur. Mais notez que vous avez les modificateurs "out" et "ref" sur les arguments.
Vous pouvez donc passer une structure par référence comme ceci:
public void fooBar( ref Vector2 position )
{
}
Oui, les structures héritent de ValueType et sont transmises par valeur. Cela est également vrai pour les types primitifs - int, double, bool, etc. (mais pas string). Les chaînes, les tableaux et toutes les classes sont des types de référence et sont transmis par référence.
Si vous voulez passer une structure par ref, en utilisant le mot clé ref
:
public void MyMethod (ref Vector2 position)
qui passera la struct by-ref, et vous permettra de modifier ses membres.
Chaque emplacement de stockage d'un type de structure contient tous les champs, privés et publics, de cette structure. Passer un paramètre d'un type de structure implique d'allouer de l'espace pour cette structure sur la pile et de copier tous les champs de la structure vers la pile.
En ce qui concerne l'utilisation des structures stockées dans une collection, l'utilisation de structures mutables avec les collections existantes nécessite généralement de lire une structure sur une copie locale, de muter cette copie et de la stocker. En supposant que MyList est un List<Point>
, et on veut ajouter une variable locale z
à MyList[3].X
:
Point temp = MaListe [3]; Temp.X + = Z; MaListe [3] = temp;
Ceci est légèrement ennuyeux, mais est souvent plus propre, plus sûr et plus efficace que l'utilisation de structures immuables, bien plus efficace que les objets de classe immuables et bien plus sûr que l'utilisation d'objets de classe mutables. J'aimerais vraiment voir le support du compilateur pour une meilleure façon pour les collections d'exposer des éléments de type valeur. Il existe des moyens d'écrire du code efficace pour gérer une telle exposition avec une bonne sémantique (par exemple, un objet de collection peut réagir lorsque les éléments sont mis à jour, sans exiger que ces éléments aient un lien vers la collection) mais le code se lit horriblement. L'ajout de la prise en charge du compilateur d'une manière conceptuellement similaire aux fermetures permettrait également à un code efficace d'être lisible.
Notez que contrairement à ce que certains prétendent, une structure est fondamentalement différent à partir d'un objet de type classe, mais pour chaque type de structure, il existe un type correspondant, parfois appelé "structure en boîte", qui dérive de Object (voir la spécification CLI (Common Language Infrastructure) , sections 8.2.4 et 8.9.7). Bien que le compilateur convertisse implicitement n'importe quelle structure en son type encadré correspondant lorsque cela est nécessaire pour le passer au code qui attend une référence à un objet de type classe, permettra aux références aux structures encadrées d'avoir leur contenu copié en structures réelles, et autorisera parfois code pour travailler directement avec des structures en boîte, les structures en boîte se comportent comme des objets de classe, car c'est ce qu'elles sont.