Jul 31, 2023

Les structures en C# sont amusantes - Partie 2/9 : Une brève introduction aux types de valeur par rapport aux types de référence

Leia este post em português

Read this post in English.

  1. Les structures en C# sont amusantes
  2. Brève introduction aux Value Types vs Reference Types (cet post).
  3. Initialisation des champs dans les structures.
  4. Comportement des constructeurs dans structures.
  5. Des autres scénarios dans lesquels le comportement des constructeurs de structure peut vous surprendre.
  6.  Struct avec des valeurs d'argument par défaut dans les constructeurs, ou, n'êtes-vous pas encore confus ?
  7. Le modificateur `required`de C # 11 ne sauvegardera pas votre c*l emploi.
  8. Structure utilisée comme valeurs d'argument défaut.
  9. Bonus: L'evolution des structures en C#.

Depuis la première version officielle (version 1.0, en janvier 2002) de C#, les développeurs ont été confrontés à une décision lors de l'introduction de nouveaux types : le déclarer comme une class (un type référence) ou une struct (un type valeur), représenté dans le système de type .NET par System.Object et System.ValueType respectivement.

Choisir l'un sur l'autre a des implications non négligeables en termes de convivialité, de performances, d'extensibilité, pour n'en citer que quelques-unes. Dans cet article, je souhaite couvrir brièvement les principales différences (plus de détails seront présentés dans les prochains articles) et clarifier une idée fausse (que je suis probablement coupable d'avoir contribué à diffuser). Alors sans plus tarder, entrons dans la distinction la plus importante entre les deux ...

Reference type versus Value type sémantique

La caractéristique la plus importante qui distingue ces 2 types concerne la manière dont l'égalité et le attribution/paramètres sont gérés. Le tableau ci-dessous montre ces différences (en supposant que les types en question ne surchargent pas la méthode Equals() ou les opérateurs ==/!=):


Value Type Reference Type
Sémantique d'attribution
par valeur, c'est-à-dire que le contenu de l'instance est copié en laissant deux copies indépendantes.
par référence, c'est-à-dire que l'attribution copie simplement une référence et que les modifications via l'une des références seront observées lors de l'accès à l'objet via l'autre.
Égalité
(Sémantique de l'identité)
deux instances sont égales si ce sont des instances du même type et si tous ses champs sont égaux. deux instances sont égales si elles référencent le même objet (c'est-à-dire exactement la même référence)

Afin de faciliter la visualisation, imaginez le code suivant :

Après avoir exécuté le code jusqu'à la ligne 15, nous pouvons représenter la mémoire utilisée par les variables v1 et v3, d'une manière trop simplifiée, comme quelque chose comme :


donc la variable v1 est stockée à l'adresse 0x100 tandis que v3 à l'adresse 0x400. Notez cependant que v3 ne stocke pas les données d'instance réelles de l'objet AReferenceType. Au lieu de cela, il stocke une référence (ou, dans une simplification, un pointeur) à l'objet réel alloué à l'adresse 0x1000. En revanche, les instances AValueType stockent leurs données en ligne.

Si nous inspectons l'état de la mémoire lorsque le programme atteint la ligne 19, nous observerons quelque chose comme:

Notez que les deux variables (v1 et v3) avaient leur contenu copié dans les nouvelles déclarées (v2 et v4) mais puisque v3 est un type de référence, copier sa valeur signifie copier une référence, un fait qui deviendra évident plus tard.

Lorsque le code imprime les champs IntValue de v1 & v2, il est évident que la même valeur sera affichée mais après avoir stocké la valeur 71 dans v2.IntValue ( à la ligne 21), la mémoire ressemblera à
:


donc la ligne 22 imprimera les valeurs 42 et 71, ou, en d'autres termes, v1 et v2 sont indépendants l'un de l'autre.

Reference types fonctionnent différemment ; commençons par la ligne 25 dans laquelle le IntValue des variables
v3 et v4 sont affichées ; afin de déterminer quelles valeurs seront transmises à la méthode Console.WriteLine(), d'abord la valeur de la variable v3 (c'est-à-dire 0x1000) est lue à partir de l'emplacement à l'adresse 0x400 (v3), puis le contenu à cette adresse (en fait le quatre octets qui composent un int en C#) est lu, produisant la valeur 42 ; ensuite, le même processus est répété pour v4, mais puisque la variable v4 fait référence au même objet que la variable v3 (0x1000), cela produira le même résultat.

Puisqu'un processus similaire est appliqué lors du changement d'état du type de référence, après l'exécution de la ligne 26, la mémoire ressemblera à
:

 

et la ligne 27 affichera 71 et 71, c'est-à-dire que les modifications appliquées à la variable v4 sont observables jusqu'à la variable v3.

Idée fausse

Au fil des ans, j'ai paru plusieurs articles, interviews et même certains livres affirmant que l'une des principales différences entre les types de valeur et les types de référence est que les types de valeur sont toujours stockés sur la pile tandis que les types de référence sont toujours stockés dans le tas géré (comme je mentionné ci-dessus, je suis très probablement coupable de propager cette idée fausse aussi :().

Cette idée fausse est tellement répandue qu'Eric Lippert (qui a travaillé comme l'un des concepteurs de C # dans le passé) a écrit 2 articles de blog pour la démystifier.

Comme le mentionne Eric, le fait que les types valeur soient généralement alloués sur la pile est un détail d'implémentation, mais a mon avis, qui ne changera jamais ; Cela dit, ce comportement de mise en œuvre est important dans les scénarios où il est essentiel de maintenir les allocations de tas (et par conséquent la pression du GC) à un niveau bas pour obtenir des caractéristiques de performances prévisibles.

Comme toujours, tous les commentaires sont bienvenus

Have fun!


No comments: