Types de référence .NET vs types de valeur. Partie 1

Parlons d'abord des types de référence et des types de valeur. Je pense que les gens ne comprennent pas vraiment les différences et les avantages des deux. Ils disent généralement que les types de référence stockent le contenu sur le tas et que les types de valeur stockent le contenu sur la pile, ce qui est faux.


Discutons des vraies différences:


  • Un type de valeur : sa valeur est une structure entière . La valeur d'un type de référence est une référence à un objet. - Une structure en mémoire: les types de valeurs contiennent uniquement les données que vous avez indiquées. Les types de référence contiennent également deux champs système. Le premier stocke «SyncBlockIndex», le second stocke les informations sur un type, y compris les informations sur une table de méthodes virtuelles (VMT).
  • Les types de référence peuvent avoir des méthodes qui sont remplacées lorsqu'elles sont héritées. Les types de valeur ne peuvent pas être hérités.
  • Vous devez allouer de l'espace sur le tas pour une instance d'un type de référence. Un type de valeur peut être alloué sur la pile ou il devient la partie d'un type de référence. Cela augmente suffisamment les performances de certains algorithmes.

Cependant, il existe des caractéristiques communes:


  • Les deux sous-classes peuvent hériter du type d'objet et devenir ses représentants.

Regardons de plus près chaque fonctionnalité.


Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.

Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository github / sidristij / dotnetbook .


Regardons de plus près chaque fonctionnalité.


Copie


La principale différence entre les deux types est la suivante:


  • Chaque variable, classe ou structure champs ou paramètres de méthode qui prennent un type de référence stockent une référence à une valeur;
  • Mais chaque variable, classe ou structure de champs ou paramètres de méthode qui prennent un type de valeur stockent une valeur exacte, c'est-à-dire une structure entière.

Cela signifie que l'attribution ou la transmission d'un paramètre à une méthode copiera la valeur. Même si vous modifiez la copie, l'original restera le même. Cependant, si vous modifiez des champs de type de référence, cela «affectera» toutes les pièces avec une référence à une instance d'un type. Regardons le
exemple:


DateTime dt = DateTime.Now; // Here, we allocate space for DateTime variable when calling a method, // but it will contain zeros. Next, let's copy all // values of the Now property to dt variable DateTime dt2 = dt; // Here, we copy the value once again object obj = new object(); // Here, we create an object by allocating memory on the Small Object Heap, // and put a pointer to the object in obj variable object obj2 = obj; // Here, we copy a reference to this object. Finally, // we have one object and two references. 

Il semble que cette propriété produise des constructions de code ambiguë telles que le
changement de code dans les collections:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create an array of such structures and initialize the Data field = 5 var array = new [] { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field array[0].Data = 4; // Let's check the value Console.WriteLine(array[0].Data); 

Il y a une petite astuce dans ce code. Il semble que nous obtenions d'abord l'instance de structure, puis attribuons une nouvelle valeur au champ Données de la copie. Cela signifie que nous devrions obtenir à nouveau 5 lors de la vérification de la valeur. Cependant, cela ne se produit pas. MSIL a une instruction distincte pour définir les valeurs des champs dans les structures d'un tableau, ce qui augmente les performances. Le code fonctionnera comme prévu: le programme
sortie 4 vers une console.


Voyons ce qui se passera si nous modifions ce code:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field list[0].Data = 4; // Let's check the value Console.WriteLine(list[0].Data); 

La compilation de ce code échouera, car lorsque vous écrivez la list[0].Data = 4 vous obtenez d'abord la copie de la structure. En fait, vous appelez une méthode d'instance de type List<T> qui sous-tend l'accès par un index. Il prend la copie d'une structure à partir d'un tableau interne ( List<T> stocke les données dans des tableaux) et vous renvoie cette copie à partir de la méthode d'accès à l'aide d'un index. Ensuite, vous essayez de modifier la copie, qui n'est pas utilisée plus loin. Ce code est inutile. Un compilateur interdit un tel comportement, sachant que les utilisateurs abusent des types de valeur. Nous devons réécrire cet exemple de la manière suivante:


 // Let's declare a structure struct ValueHolder { public int Data; } // Let's create a list of such structures and initialize the Data field = 5 var list = new List<ValueHolder> { new ValueHolder { Data = 5 } }; // Let's use an index to get the structure and put 4 in the Data field. Then, let's save it again. var copy = list[0]; copy.Data = 4; list[0] = copy; // Let's check the value Console.WriteLine(list[0].Data); 

Ce code est correct malgré sa redondance apparente. Le programme
sortie 4 vers une console.


L'exemple suivant montre ce que j'entends par «la valeur d'une structure est un
structure entière »


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } int x = 5; PersonInfo person; int y = 6; // Variant 2 int x = 5; int Height; int Width; int HairColor; int y = 6; 

Les deux exemples sont similaires en termes d'emplacement des données en mémoire, car la valeur de la structure est la structure entière. Il alloue la mémoire pour elle-même où elle se trouve.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } class Employee { public int x; public PersonInfo person; public int y; } // Variant 2 class Employee { public int x; public int Height; public int Width; public int HairColor; public int y; } 

Ces exemples sont également similaires en termes de localisation des éléments en mémoire car la structure prend une place définie parmi les champs de classe. Je ne dis pas qu'ils sont totalement similaires car vous pouvez utiliser des champs de structure en utilisant des méthodes de structure.


Bien sûr, ce n'est pas le cas des types de référence. Une instance elle-même se trouve sur le petit tas d'objets inaccessibles (SOH) ou le grand tas d'objets (LOH). Un champ de classe contient uniquement la valeur d'un pointeur vers une instance: un nombre 32 ou 64 bits.


Regardons le dernier exemple pour fermer le problème.


 // Variant 1 struct PersonInfo { public int Height; public int Width; public int HairColor; } void Method(int x, PersonInfo person, int y); // Variant 2 void Method(int x, int HairColor, int Width, int Height, int y); 

En termes de mémoire, les deux variantes de code fonctionneront de manière similaire, mais pas en termes d'architecture. Il ne s'agit pas seulement de remplacer un nombre variable d'arguments. L'ordre change car les paramètres de méthode sont déclarés les uns après les autres. Ils sont placés sur la pile de la même manière.


Cependant, la pile passe d'adresses supérieures à des adresses inférieures. Cela signifie que l'ordre de pousser une structure pièce par pièce sera différent de la pousser dans son ensemble.


Méthodes remplaçables et héritage


La prochaine grande différence entre les deux types est le manque de virtuel
table de méthodes dans les structures. Cela signifie que:


  1. Vous ne pouvez pas décrire et remplacer des méthodes virtuelles dans des structures.
  2. Une structure ne peut en hériter une autre. La seule façon d'émuler l'héritage est de mettre une structure de type de base dans le premier champ. Les champs d'une structure «héritée» iront après les champs d'une structure «de base» et créeront un héritage logique. Les champs des deux structures coïncideront en fonction du décalage.
  3. Vous pouvez passer des structures à du code non managé. Cependant, vous perdrez les informations sur les méthodes. En effet, une structure n'est qu'un espace en mémoire, rempli de données sans les informations sur un type. Vous pouvez le transmettre à des méthodes non managées, par exemple, écrites en C ++, sans modifications.

L'absence d'une table de méthodes virtuelles soustrait une certaine partie de la «magie» de l'héritage aux structures mais leur donne d'autres avantages. Le premier est que nous pouvons transmettre des instances d'une telle structure à des environnements externes (en dehors de .NET Framework). Rappelez-vous, ce n'est qu'un souvenir
gamme! Nous pouvons également prendre une plage de mémoire de code non managé et convertir un type à notre structure pour rendre ses champs plus accessibles. Vous ne pouvez pas faire cela avec des classes car elles ont deux champs inaccessibles. Il s'agit de SyncBlockIndex et d'une adresse de table de méthodes virtuelles. Si ces deux champs passent au code non managé, ce sera dangereux. En utilisant un tableau de méthodes virtuelles, on peut accéder à n'importe quel type et le modifier pour attaquer une application.


Montrons qu'il ne s'agit que d'une plage de mémoire sans logique supplémentaire.


 unsafe void Main() { int secret = 666; HeightHolder hh; hh.Height = 5; WidthHolder wh; unsafe { // This cast wouldn't work if structures had the information about a type. // The CLR would check a hierarchy before casting a type and if it didn't find WidthHolder, // it would output an InvalidCastException exception. But since a structure is a memory range, // you can interpret it as any kind of structure. wh = *(WidthHolder*)&hh; } Console.WriteLine("Width: " + wh.Width); Console.WriteLine("Secret:" + wh.Secret); } struct WidthHolder { public int Width; public int Secret; } struct HeightHolder { public int Height; } 

Ici, nous effectuons l'opération impossible en typage fort. Nous convertissons un type en un autre incompatible qui contient un champ supplémentaire. Nous introduisons une variable supplémentaire à l'intérieur de la méthode Main. En théorie, sa valeur est secrète. Cependant, l'exemple de code affichera la valeur d'une variable, introuvable dans aucune des structures à l'intérieur de la méthode Main() . Vous pouvez le considérer comme une atteinte à la sécurité, mais les choses ne sont pas si simples. Vous ne pouvez pas vous débarrasser du code non managé dans un programme. La raison principale est la structure de la pile de threads. On peut l'utiliser pour accéder au code non managé et jouer avec des variables locales. Vous pouvez défendre votre code contre ces attaques en randomisant la taille d'un cadre de pile. Ou, vous pouvez supprimer les informations sur le registre EBP pour compliquer le retour d'une trame de pile. Cependant, cela n'a plus d'importance pour nous maintenant. Ce qui nous intéresse dans cet exemple est le suivant. La variable "secrète" va avant la définition de la variable hh et ensuite dans la structure WidthHolder (à différents endroits, en fait). Alors, pourquoi avons-nous facilement obtenu sa valeur? La réponse est que la pile croît de droite à gauche. Les variables déclarées en premier auront des adresses beaucoup plus élevées, et celles déclarées plus tard auront des adresses inférieures.


Le comportement lors de l'appel de méthodes d'instance


Les deux types de données ont une autre caractéristique qui n'est pas évidente à voir et peut expliquer la structure des deux types. Il traite de l'appel des méthodes d'instance.


 // The example with a reference type class FooClass { private int x; public void ChangeTo(int val) { x = val; } } // The example with a value type struct FooStruct { private int x; public void ChangeTo(int val) { x = val; } } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); klass.ChangeTo(10); strukt.ChangeTo(10); 

Logiquement, nous pouvons décider que la méthode a un corps compilé. En d'autres termes, il n'y a pas d'instance d'un type qui possède son propre ensemble de méthodes compilé, semblable aux ensembles d'autres instances. Cependant, la méthode appelée sait à quelle instance elle appartient car une référence à l'instance d'un type est le premier paramètre. Nous pouvons réécrire notre exemple et il sera identique à ce que nous avons dit auparavant. Je n'utilise pas délibérément un exemple avec des méthodes virtuelles, car elles ont une autre procédure.


 // An example with a reference type class FooClass { public int x; } // An example with a value type struct FooStruct { public int x; } public void ChangeTo(FooClass klass, int val) { klass.x = val; } public void ChangeTo(ref FooStruct strukt, int val) { strukt.x = val; } FooClass klass = new FooClass(); FooStruct strukt = new FooStruct(); ChangeTo(klass, 10); ChangeTo(ref strukt, 10); 

Je devrais expliquer l'utilisation du mot-clé ref. Si je ne l'utilisais pas, j'obtiendrais une copie de la structure comme paramètre de méthode au lieu de l'original. Ensuite, je le changerais, mais l'original resterait le même. Je devrais retourner une copie modifiée d'une méthode à un appelant (une autre copie), et l'appelant enregistrerait cette valeur dans la variable (une copie de plus). Au lieu de cela, une méthode d'instance obtient un pointeur et l'utilise pour changer immédiatement l'original. L'utilisation d'un pointeur n'influence pas les performances car toutes les opérations au niveau du processeur utilisent des pointeurs. Ref fait partie du monde C #, pas plus.


La capacité de pointer vers la position des éléments.


Les structures et les classes ont une autre capacité de pointer vers le décalage d'un champ particulier par rapport au début d'une structure en mémoire. Cela sert à plusieurs fins:


  • de travailler avec des API externes dans le monde non managé sans avoir à insérer les champs inutilisés avant celui nécessaire;
  • pour demander à un compilateur de localiser un champ juste au début du type ( [FieldOffset(0)] ). Cela rendra le travail avec ce type plus rapide. S'il s'agit d'un champ fréquemment utilisé, nous pouvons augmenter les performances de l'application. Cependant, cela n'est vrai que pour les types de valeur. Dans les types de référence, le champ avec un décalage nul contient l'adresse d'une table de méthodes virtuelle, qui prend 1 mot machine. Même si vous adressez le premier champ d'une classe, il utilisera un adressage complexe (adresse + offset). En effet, le champ de classe le plus utilisé est l'adresse d'une table de méthodes virtuelles. La table est nécessaire pour appeler toutes les méthodes virtuelles;
  • pour pointer vers plusieurs champs en utilisant une seule adresse. Dans ce cas, la même valeur est interprétée comme différents types de données. En C ++, ce type de données est appelé union;
  • ne pas se soucier de déclarer quoi que ce soit: un compilateur allouera les champs de manière optimale. Ainsi, l'ordre final des champs peut être différent.

Remarques générales


  • Auto : l'environnement d'exécution choisit automatiquement un emplacement et un emballage pour tous les champs de classe ou de structure. Les structures définies qui sont marquées par un membre de cette énumération ne peuvent pas passer en code non managé. La tentative de le faire produira une exception;
  • Explicite : un programmeur contrôle explicitement l'emplacement exact de chaque champ d'un type avec FieldOffsetAttribute;
  • Séquentiel : les membres de type viennent dans un ordre séquentiel, défini lors de la conception du type. La valeur StructLayoutAttribute.Pack d'une étape de conditionnement indique leur emplacement.

Utilisation de FieldOffset pour ignorer les champs de structure inutilisés


Les structures provenant du monde non managé peuvent contenir des champs réservés. On peut les utiliser dans une future version d'une bibliothèque. En C / C ++, nous comblons ces lacunes en ajoutant des champs, par exemple [StructLayout(LayoutKind.Explicit)] , ... Cependant, en .NET, nous nous contentons de compenser au début d'un champ en utilisant l'attribut FieldOffsetAttribute et [StructLayout(LayoutKind.Explicit)] .


 [StructLayout(LayoutKind.Explicit)] public struct SYSTEM_INFO { [FieldOffset(0)] public ulong OemId; // 92 bytes reserved [FieldOffset(100)] public ulong PageSize; [FieldOffset(108)] public ulong ActiveProcessorMask; [FieldOffset(116)] public ulong NumberOfProcessors; [FieldOffset(124)] public ulong ProcessorType; } 

Un espace est occupé mais l'espace inutilisé. La structure aura la taille égale à 132 et non 40 octets comme cela peut sembler au début.


Union


À l'aide de FieldOffsetAttribute, vous pouvez émuler le type C / C ++ appelé union. Il permet d'accéder aux mêmes données que les entités de
différents types. Regardons l'exemple:


 // If we read the RGBA.Value, we will get an Int32 value accumulating all // other fields. // However, if we try to read the RGBA.R, RGBA.G, RGBA.B, RGBA.Alpha, we // will get separate components of Int32. [StructLayout(LayoutKind.Explicit)] public struct RGBA { [FieldOffset(0)] public uint Value; [FieldOffset(0)] public byte R; [FieldOffset(1)] public byte G; [FieldOffset(2)] public byte B; [FieldOffset(3)] public byte Alpha; } 

Vous pourriez dire qu'un tel comportement n'est possible que pour les types de valeur. Cependant, vous pouvez le simuler pour les types de référence, en utilisant une adresse pour chevaucher deux types de référence ou un type de référence et un type de valeur:


 class Program { public static void Main() { Union x = new Union(); x.Reference.Value = "Hello!"; Console.WriteLine(x.Value.Value); } [StructLayout(LayoutKind.Explicit)] public class Union { public Union() { Value = new Holder<IntPtr>(); Reference = new Holder<object>(); } [FieldOffset(0)] public Holder<IntPtr> Value; [FieldOffset(0)] public Holder<object> Reference; } public class Holder<T> { public T Value; } } 

J'ai utilisé un type générique pour se chevaucher exprès. Si j'ai utilisé d'habitude
qui se chevauchent, ce type entraînerait l'exception TypeLoadException lorsqu'il est chargé dans un domaine d'application. Cela peut ressembler à une faille de sécurité en théorie (en particulier lorsque l'on parle de plug-ins d' application), mais si nous essayons d'exécuter ce code à l'aide d'un domaine protégé, nous obtiendrons la même TypeLoadException .


La différence d'allocation


Une autre caractéristique qui différencie les deux types est l'allocation de mémoire pour les objets ou les structures. Le CLR doit décider de plusieurs choses avant d'allouer de la mémoire à un objet. Quelle est la taille d'un objet? Est-ce plus ou moins de 85K? S'il est inférieur, y a-t-il suffisamment d'espace libre sur le SOH pour allouer cet objet? Si c'est plus, le CLR active le garbage collector. Il passe par un graphique d'objet, compacte les objets en les déplaçant vers l'espace dégagé. S'il n'y a toujours pas d'espace sur le SOH, l'allocation de pages de mémoire virtuelle supplémentaires commencera. Ce n'est qu'alors qu'un objet obtient l'espace alloué, débarrassé des ordures. Ensuite, le CLR présente SyncBlockIndex et VirtualMethodsTable. Enfin, la référence à un objet revient à un utilisateur.


Si un objet alloué est plus grand que 85 Ko, il va au tas d'objets volumineux (LOH). C'est le cas des grandes chaînes et des tableaux. Ici, nous devons trouver l'espace le plus approprié en mémoire dans la liste des plages inoccupées ou en allouer une nouvelle. Ce n'est pas rapide, mais nous allons traiter avec soin les objets de cette taille. De plus, nous n'allons pas en parler ici.


Il existe plusieurs scénarios possibles pour les RefTypes:


  • RefType <85K, il y a de l'espace sur le SOH: allocation rapide de mémoire;
  • RefType <85K, l'espace sur le SOH est épuisé: allocation de mémoire très lente;
  • RefType> 85K, allocation de mémoire lente.

De telles opérations sont rares et ne peuvent pas rivaliser avec ValTypes. L'algorithme d'allocation de mémoire pour les types de valeur n'existe pas. L'allocation de mémoire pour les types de valeur ne coûte rien. La seule chose qui se produit lors de l'allocation de mémoire pour ce type est de définir les champs sur null. Voyons pourquoi cela se produit: 1. Lorsque l'on déclare une variable dans le corps d'une méthode, le temps d'allocation de mémoire pour une structure est proche de zéro. En effet, le temps d'allocation des variables locales ne dépend pas de leur nombre; 2. Si ValTypes sont alloués en tant que champs, Reftypes augmentera la taille des champs. Un type de valeur est alloué entièrement, devenant sa partie; 3. Comme dans le cas de la copie, si les ValTypes sont passés en tant que paramètres de méthode, il apparaît une différence, selon la taille et l'emplacement d'un paramètre.


Cependant, cela ne prend pas plus de temps que de copier une variable dans une autre.


Le choix entre une classe ou une structure


Discutons des avantages et des inconvénients des deux types et décidons de leurs scénarios d'utilisation. Un principe classique dit que nous devons choisir un type de valeur s'il n'est pas supérieur à 16 octets, reste inchangé pendant sa durée de vie et n'est pas hérité. Cependant, choisir le bon type signifie le revoir sous différentes perspectives en se basant sur des scénarios d'utilisation future. Je propose trois groupes de critères:


  • basé sur l'architecture du système de type, dans lequel votre type interagira;
  • en fonction de votre approche en tant que programmeur système pour choisir un type avec des performances optimales;
  • quand il n'y a pas d'autre choix.

Chaque fonctionnalité conçue doit refléter son objectif. Cela ne concerne pas uniquement son nom ou son interface d'interaction (méthodes, propriétés). On peut utiliser des considérations architecturales pour choisir entre les types de valeur et de référence. Pensons pourquoi une structure et non une classe pourrait être choisie du point de vue du système de type système.


  1. Si votre type conçu est agnostique à son état, cela signifie que son état reflète un processus ou est une valeur de quelque chose. En d'autres termes, une instance d'un type est constante et immuable par nature. Nous pouvons créer une autre instance d'un type basé sur cette constante en indiquant un décalage. Ou, nous pouvons créer une nouvelle instance en indiquant ses propriétés. Cependant, nous ne devons pas le changer. Je ne veux pas dire que la structure est un type immuable. Vous pouvez modifier ses valeurs de champ. De plus, vous pouvez passer une référence à une structure dans une méthode en utilisant le paramètre ref et vous obtiendrez des champs modifiés après avoir quitté la méthode. Ce dont je parle ici, c'est du sens architectural. Je vais donner plusieurs exemples.


    • DateTime est une structure qui résume le concept d'un moment dans le temps. Il stocke ces données sous la forme d'une uint mais donne accès à des caractéristiques distinctes d'un moment dans le temps: année, mois, jour, heure, minutes, secondes, millisecondes et même ticks du processeur. Cependant, il est immuable, basé sur ce qu'il encapsule. Nous ne pouvons pas changer un instant dans le temps. Je ne peux pas vivre la minute suivante comme si c'était mon meilleur anniversaire dans l'enfance. Ainsi, si nous choisissons un type de données, nous pouvons choisir une classe avec une interface en lecture seule, qui produit une nouvelle instance pour chaque changement de propriétés. Ou, nous pouvons choisir une structure, qui peut mais ne doit pas changer les champs de ses instances: sa valeur est la description d'un moment dans le temps, comme un nombre. Vous ne pouvez pas accéder à la structure d'un numéro et la modifier. Si vous souhaitez obtenir un autre moment dans le temps, qui diffère d'un jour de l'original, vous obtiendrez simplement une nouvelle instance d'une structure.
    • KeyValuePair<TKey, TValue> est une structure qui encapsule le concept d'une paire clé-valeur connectée. Cette structure sert uniquement à afficher le contenu d'un dictionnaire pendant l'énumération. Du point de vue architectural, une clé et une valeur sont des concepts indissociables dans Dictionary<T> . Cependant, à l'intérieur, nous avons une structure complexe, où une clé se trouve séparément d'une valeur. Pour un utilisateur, une paire clé-valeur est un concept indissociable en termes d'interface et de signification d'une structure de données. C'est une valeur entière en soi. Si l'on attribue une autre valeur à une clé, la paire entière changera. Ainsi, ils représentent une seule entité. Cela fait d'une structure une variante idéale dans ce cas.

  2. Si votre type conçu est une partie inséparable d'un type externe mais est structurellement intégré. Cela signifie qu'il est incorrect de dire que le type externe fait référence à une instance d'un type encapsulé. Cependant, il est correct de dire qu'un type encapsulé fait partie d'un externe avec toutes ses propriétés. Ceci est utile lors de la conception d'une structure qui fait partie d'une autre structure.


    • Par exemple, si nous prenons la structure d'un en-tête de fichier, il sera inapproprié de passer une référence d'un fichier à un autre, par exemple un fichier header.txt. Cela serait approprié lors de l'insertion d'un document dans un autre, non pas en incorporant un fichier mais en utilisant une référence dans un système de fichiers. Un bon exemple est les fichiers de raccourcis dans le système d'exploitation Windows. Cependant, si nous parlons d'un en-tête de fichier (par exemple, un en-tête de fichier JPEG contenant des métadonnées sur la taille d'une image, les méthodes de compression, les paramètres de photographie, les coordonnées GPS et autres), nous devons utiliser des structures pour concevoir des types d'analyse de l'en-tête. Si vous décrivez tous les en-têtes dans les structures, vous obtiendrez la même position des champs en mémoire que dans un fichier. En utilisant une simple transformation *(Header *)readedBuffer non sécurisée *(Header *)readedBuffer sans désérialisation, vous obtiendrez des structures de données entièrement remplies.


  1. Aucun des deux exemples ne montre l'héritage du comportement. Ils montrent qu'il n'est pas nécessaire d'hériter du comportement de ces entités. Ils sont autonomes. Cependant, si nous prenons en compte l'efficacité du code, nous verrons le choix d'un autre côté:
  2. Si nous devons prendre des données structurées à partir de code non managé, nous devons choisir des structures. Nous pouvons également transmettre la structure des données à une méthode non sécurisée. Un type de référence ne convient pas du tout.
  3. Une structure est votre choix si un type transmet les données dans des appels de méthode (en tant que valeurs renvoyées ou en tant que paramètre de méthode) et qu'il n'est pas nécessaire de faire référence à la même valeur à différents endroits. L'exemple parfait est les tuples. Si une méthode renvoie plusieurs valeurs à l'aide de tuples, elle renverra un ValueTuple, déclaré en tant que structure. La méthode n'allouera pas d'espace sur le tas, mais utilisera la pile du thread, où l'allocation de mémoire ne coûte rien.
  4. Si vous concevez un système qui crée un trafic important d'instances qui ont une taille et une durée de vie réduites, l'utilisation de types de référence entraînera soit un pool d'objets, soit, sans le pool d'objets, une accumulation de déchets incontrôlée sur le tas. Certains objets se transformeront en générations plus anciennes, augmentant la charge sur GC. L'utilisation de types de valeurs dans de tels endroits (si c'est possible) augmentera les performances car rien ne passera au SOH. Cela réduira la charge sur GC et l'algorithme fonctionnera plus rapidement;

Sur la base de ce que j'ai dit, voici quelques conseils sur l'utilisation des structures:


  1. Lorsque vous choisissez des collections, vous devez éviter que les grands tableaux stockent de grandes structures. Cela inclut les structures de données basées sur des tableaux. Cela peut entraîner une transition vers le tas d'objets volumineux et sa fragmentation. Il est faux de penser que si notre structure a 4 champs de type octet, cela prendra 4 octets. Nous devons comprendre que dans les systèmes 32 bits, chaque champ de structure est aligné sur des limites de 4 octets (chaque champ d'adresse doit être divisé exactement par 4) et dans les systèmes 64 bits - sur des limites de 8 octets. La taille d'un tableau doit dépendre de la taille d'une structure et d'une plate-forme exécutant un programme. Dans notre exemple avec 4 octets - 85 Ko / (de 4 à 8 octets par champ * le nombre de champs = 4) moins la taille d'un en-tête de tableau équivaut à environ 2 600 éléments par tableau selon la plate-forme (cela doit être arrondi vers le bas ) Ce n'est pas beaucoup. Il pouvait sembler que nous pouvions facilement atteindre une constante magique de 20 000 éléments
  2. Parfois, vous utilisez une structure de grande taille comme source de données et la placez comme champ dans une classe, tout en ayant une copie répliquée pour produire un millier d'instances. Ensuite, vous développez chaque instance d'une classe pour la taille d'une structure. Cela entraînera un gonflement de la génération zéro et une transition vers la génération un et même deux. Si les instances d'une classe ont une courte durée de vie et que vous pensez que le GC les collectera à la génération zéro - pendant 1 ms, vous serez déçu. Ils sont déjà en génération un et même deux. Cela fait la différence. Si le GC collecte la génération zéro pendant 1 ms, les générations une et deux sont collectées très lentement, ce qui entraînera une diminution de l'efficacité;
  3. Pour la même raison, vous devez éviter de passer de grandes structures via une série d'appels de méthode. Si tous les éléments s'appellent les uns les autres, ces appels prendront plus d'espace sur la pile et entraîneront la mort de votre application par StackOverflowException. La prochaine raison est la performance. Plus il y a d'exemplaires, plus tout fonctionne lentement.

C'est pourquoi le choix d'un type de données n'est pas un processus évident. Souvent, cela peut faire référence à une optimisation prématurée, ce qui n'est pas recommandé. Cependant, si vous savez que votre situation correspond aux principes énoncés ci-dessus, vous pouvez facilement choisir un type de valeur.


Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.

Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository github / sidristij / dotnetbook .

Source: https://habr.com/ru/post/fr439486/


All Articles