
Type de base d'objet et implémentation d'interfaces. La boxe
Il semble que nous ayons traversé l'enfer et les hautes eaux et que nous puissions réussir n'importe quelle interview, même celle de l'équipe .NET CLR. Cependant, ne nous précipitons pas sur microsoft.com et recherchons des postes vacants. Maintenant, nous devons comprendre comment les types de valeurs héritent d'un objet s'ils ne contiennent ni référence à SyncBlockIndex, ni pointeur vers une table de méthodes virtuelles. Cela expliquera complètement notre système de types et toutes les pièces d'un puzzle trouveront leur place. Cependant, nous aurons besoin de plus d'une phrase.
Maintenant, rappelons-nous à nouveau comment les types de valeurs sont alloués en mémoire. Ils ont la place en mémoire là où ils se trouvent. Les types de référence obtiennent une allocation sur le tas d'objets petits et grands. Ils donnent toujours une référence à l'endroit sur le tas où se trouve l'objet. Chaque type de valeur a des méthodes telles que ToString, Equals et GetHashCode. Ils sont virtuels et remplaçables, mais ne permettent pas d'hériter d'un type de valeur en remplaçant les méthodes. Si les types de valeur utilisaient des méthodes remplaçables, ils auraient besoin d'une table de méthodes virtuelle pour router les appels. Cela entraînerait des problèmes de passage des structures dans un monde non géré: des champs supplémentaires y iraient. Par conséquent, il existe des descriptions de méthodes de type valeur quelque part, mais vous ne pouvez pas y accéder directement via une table de méthodes virtuelle.
Cela peut amener l'idée que le manque d'héritage est artificiel
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 .
Cela peut amener l'idée que le manque d'héritage est artificiel:
- il y a héritage d'un objet, mais pas direct;
- il y a ToString, Equals et GetHashCode dans un type de base. Dans les types de valeur, ces méthodes ont leur propre comportement. Cela signifie que les méthodes sont remplacées par rapport à un
object
; - de plus, si vous transtypez un type en un
object
, vous avez le droit d'appeler ToString, Equals et GetHashCode; - lors de l'appel d'une méthode d'instance pour un type de valeur, la méthode obtient une autre structure qui est une copie d'un original. Cela signifie que l'appel d'une méthode d'instance revient à appeler une méthode statique:
Method(ref structInstance, newInternalFieldValue)
. En effet, cet appel réussit, à une exception près. Un JIT doit compiler le corps d'une méthode, il serait donc inutile de décaler les champs de structure, en sautant par dessus le pointeur vers une table de méthodes virtuelle, qui n'existe pas dans la structure. Il existe pour les types de valeur dans un autre endroit .
Les types ont un comportement différent, mais cette différence n'est pas si grande au niveau de l'implémentation dans le CLR. Nous en parlerons un peu plus tard.
Écrivons la ligne suivante dans notre programme:
var obj = (object)10;
Cela nous permettra de traiter le numéro 10 en utilisant une classe de base. C'est ce qu'on appelle la boxe. Cela signifie que nous avons un VMT pour appeler des méthodes virtuelles telles que ToString (), Equals et GetHashCode. En réalité, la boxe crée une copie d'un type de valeur, mais pas un pointeur vers un original. En effet, nous pouvons stocker la valeur d'origine partout: sur la pile ou en tant que champ d'une classe. Si nous le convertissons en type d'objet, nous pouvons stocker une référence à cette valeur aussi longtemps que nous le souhaitons. Quand la boxe arrive:
- le CLR alloue de l'espace sur le tas pour une structure + SyncBlockIndex + VMT d'un type valeur (pour appeler ToString, GetHashCode, Equals);
- il y copie une instance d'un type de valeur.
Maintenant, nous avons une variante de référence d'un type de valeur. Une structure a absolument le même ensemble de champs système qu'un type de référence ,
devenir un type de référence à part entière après la boxe. La structure est devenue une classe. Appelons cela un saut périlleux .NET. Ceci est un nom juste.
Regardez ce qui se passe si vous utilisez une structure qui implémente une interface utilisant la même interface.
struct Foo : IBoo { int x; void Boo() { x = 666; } } IBoo boo = new Foo(); boo.Boo();
Lorsque nous créons l'instance Foo, sa valeur va en fait à la pile. Ensuite, nous mettons cette variable dans une variable de type interface et la structure dans une variable de type référence. Ensuite, il y a la boxe et nous avons le type d'objet en sortie. Mais c'est une variable de type interface. Cela signifie que nous avons besoin d'une conversion de type. Ainsi, l'appel se déroule de la manière suivante:
IBoo boo = (IBoo)(box_to_object)new Foo(); boo.Boo();
L'écriture d'un tel code n'est pas efficace. Vous devrez modifier une copie au lieu d'un original:
void Main() { var foo = new Foo(); foo.a = 1; Console.WriteLite(foo.a); // -> 1 IBoo boo = foo; boo.Boo(); // looks like changing foo.a to 10 Console.WriteLite(foo.a); // -> 1 } struct Foo: IBoo { public int a; public void Boo() { a = 10; } } interface IBoo { void Boo(); }
La première fois que nous examinons le code, nous n'avons pas besoin de savoir ce que nous traitons dans le code autre que le nôtre et de voir une distribution vers l'interface IBoo. Cela nous fait penser que Foo est une classe et non une structure. Ensuite, il n'y a pas de division visuelle dans les structures et les classes, ce qui nous fait penser
les résultats de la modification de l'interface doivent entrer dans foo, ce qui ne se produit pas car boo est une copie de foo. C'est trompeur. À mon avis, ce code devrait recevoir des commentaires, afin que d'autres développeurs puissent y faire face.
La deuxième chose concerne les pensées précédentes selon lesquelles nous pouvons convertir un type d'un objet en IBoo. Ceci est une autre preuve qu'un type de valeur encadré est une variante de référence d'un type de valeur. Ou, tous les types d'un système de types sont des types de référence. Nous pouvons simplement travailler avec des structures comme avec des types de valeur, en passant entièrement leur valeur. Déréférencer un pointeur sur un objet comme vous le diriez dans le monde du C ++.
Vous pouvez objecter que si c'était vrai, cela ressemblerait à ceci:
var referenceToInteger = (IInt32)10;
Nous obtiendrions non seulement un objet, mais une référence typée pour un type de valeur encadrée. Cela détruirait l'idée globale des types de valeur (c'est-à-dire l'intégrité de leur valeur) permettant une grande optimisation, en fonction de leurs propriétés. Reprenons cette idée!
public sealed class Boxed<T> { public T Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(object obj) { return Value.Equals(obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() { return Value.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { return Value.GetHashCode(); } }
Nous avons un analogue complet de la boxe. Cependant, nous pouvons changer son contenu en appelant des méthodes d'instance. Ces modifications affecteront toutes les parties avec une référence à cette structure de données.
var typedBoxing = new Boxed<int> { Value = 10 }; var pureBoxing = (object)10;
La première variante n'est pas très attractive. Au lieu de lancer un type, nous créons un non-sens. La deuxième ligne est bien meilleure, mais les deux lignes sont presque identiques. La seule différence est qu'il n'y a pas de nettoyage de la mémoire avec des zéros pendant la boxe habituelle après l'allocation de mémoire sur le tas. La structure nécessaire prend tout de suite la mémoire alors que la première variante a besoin d'être nettoyée. Cela le rend plus long que la boxe habituelle de 10%.
Au lieu de cela, nous pouvons appeler certaines méthodes pour notre valeur encadrée.
struct Foo { public int x; public void ChangeTo(int newx) { x = newx; } } var boxed = new Boxed<Foo> { Value = new Foo { x = 5 } }; boxed.Value.ChangeTo(10); var unboxed = boxed.Value;
Nous avons un nouvel instrument. Pensons à ce que nous pouvons en faire.
- Notre type
Boxed<T>
fait la même chose que le type habituel: alloue de la mémoire sur le tas, y passe une valeur et permet de l'obtenir, en faisant une sorte de unbox; - Si vous perdez une référence à une structure en boîte, le GC la collectera;
- Cependant, nous pouvons maintenant travailler avec un type encadré, c'est-à-dire appeler ses méthodes;
- De plus, nous pouvons remplacer une instance d'un type de valeur dans SOH / LOH par une autre. Nous ne pouvions pas le faire avant, car nous devions faire du déballage, changer la structure en une autre et faire de la boxe, donnant une nouvelle référence aux clients.
Le principal problème de la boxe est de créer du trafic en mémoire. Le trafic d'un nombre inconnu d'objets, dont la partie peut survivre jusqu'à la première génération, où nous rencontrons des problèmes avec la collecte des ordures. Il y aura beaucoup de déchets et nous aurions pu les éviter. Mais lorsque nous avons le trafic d'objets de courte durée, la première solution est la mutualisation. Il s'agit d'une fin idéale du saut périlleux .NET.
var pool = new Pool<Boxed<Foo>>(maxCount:1000); var boxed = pool.Box(10); boxed.Value=70; // use boxed value here pool.Free(boxed);
Désormais, la boxe peut fonctionner à l'aide d'un pool, ce qui élimine le trafic mémoire lors de la boxe. Nous pouvons même faire revivre les objets dans la méthode de finalisation et les replacer dans la piscine. Cela peut être utile lorsqu'une structure encadrée passe à un code asynchrone autre que le vôtre et que vous ne pouvez pas comprendre quand cela est devenu inutile. Dans ce cas, il reviendra au pool pendant le GC.
Concluons:
- Si la boxe est accidentelle et ne devrait pas se produire, ne le faites pas. Cela peut entraîner des problèmes de performances.
- Si la boxe est nécessaire à l'architecture d'un système, il peut y avoir des variantes. Si le trafic des structures en boîte est petit et presque invisible, vous pouvez utiliser la boxe. Si le trafic est visible, vous voudrez peut-être faire le regroupement de la boxe, en utilisant l'une des solutions énoncées ci-dessus. Il dépense quelques ressources, mais fait fonctionner GC sans surcharge;
En fin de compte, regardons un code totalement impraticable:
static unsafe void Main() { // here we create boxed int object boxed = 10; // here we get the address of a pointer to a VMT var address = (void**)EntityPtr.ToPointerWithOffset(boxed); unsafe { // here we get a Virtual Methods Table address var structVmt = typeof(SimpleIntHolder).TypeHandle.Value.ToPointer(); // change the VMT address of the integer passed to Heap into a VMT SimpleIntHolder, turning Int into a structure *address = structVmt; } var structure = (IGetterByInterface)boxed; Console.WriteLine(structure.GetByInterface()); } interface IGetterByInterface { int GetByInterface(); } struct SimpleIntHolder : IGetterByInterface { public int value; int IGetterByInterface.GetByInterface() { return value; } }
Le code utilise une petite fonction, qui peut obtenir un pointeur d'une référence à un objet. La bibliothèque est disponible à l' adresse github . Cet exemple montre que la boxe habituelle se transforme en un type de référence typé. On y va
regardez les étapes du processus:
- Faites de la boxe pour un entier.
- Obtenir l'adresse d'un objet obtenu (l'adresse de Int32 VMT)
- Obtenez le VMT d'un SimpleIntHolder
- Remplacez le VMT d'un entier encadré par le VMT d'une structure.
- Transformer unboxing en type de structure
- Afficher la valeur du champ à l'écran, obtenant l'Int32, qui était
en boîte.
Je le fais via l'interface exprès car je veux montrer que cela fonctionnera
de cette façon.
Nullable \ <T>
Il convient de mentionner le comportement de la boxe avec les types de valeur Nullable. Cette fonctionnalité des types de valeur Nullable est très intéressante car la boxe d'un type de valeur qui est une sorte de null renvoie null.
int? x = 5; int? y = null; var boxedX = (object)x; // -> 5 var boxedY = (object)y; // -> null
Cela nous amène à une conclusion particulière: comme null n'a pas de type, le
la seule façon d'obtenir un type différent de celui en boîte est la suivante:
int? x = null; var pseudoBoxed = (object)x; double? y = (double?)pseudoBoxed;
Le code fonctionne simplement parce que vous pouvez convertir un type en tout ce que vous aimez
avec null.
Aller plus loin dans la boxe
Pour terminer, je voudrais vous parler du type System.Enum . Logiquement, cela devrait être un type de valeur car c'est une énumération habituelle: aliaser des nombres en noms dans un langage de programmation. Toutefois, System.Enum est un type de référence. Tous les types de données enum, définis dans votre champ ainsi que dans .NET Framework sont hérités de System.Enum. C'est un type de données de classe. De plus, c'est une classe abstraite, héritée de System.ValueType
.
[Serializable] [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { // ... }
Cela signifie-t-il que toutes les énumérations sont allouées sur le SOH et lorsque nous les utilisons, nous surchargeons le tas et le GC? En fait non, car nous les utilisons simplement. Ensuite, nous supposons qu'il y a un pool d'énumérations quelque part et nous obtenons simplement leurs instances. Non, encore une fois. Vous pouvez utiliser des énumérations dans les structures lors du marshaling. Les énumérations sont des nombres habituels.
La vérité est que CLR pirate la structure du type de données lors de sa formation s'il existe une énumération transformant une classe en type de valeur :
// Check to see if the class is a valuetype; but we don't want to mark System.Enum // as a ValueType. To accomplish this, the check takes advantage of the fact // that System.ValueType and System.Enum are loaded one immediately after the // other in that order, and so if the parent MethodTable is System.ValueType and // the System.Enum MethodTable is unset, then we must be building System.Enum and // so we don't mark it as a ValueType. if(HasParent() && ((g_pEnumClass != NULL && GetParentMethodTable() == g_pValueTypeClass) || GetParentMethodTable() == g_pEnumClass)) { bmtProp->fIsValueClass = true; HRESULT hr = GetMDImport()->GetCustomAttributeByName(bmtInternal->pType->GetTypeDefToken(), g_CompilerServicesUnsafeValueTypeAttribute, NULL, NULL); IfFailThrow(hr); if (hr == S_OK) { SetUnsafeValueClass(); } }
Pourquoi faire ça? En particulier, parce que l'idée d'héritage - pour faire une énumération personnalisée, vous devez, par exemple, spécifier les noms des valeurs possibles. Cependant, il est impossible d'hériter des types de valeur. Ainsi, les développeurs l'ont conçu pour être un type de référence qui peut le transformer en type de valeur lors de sa compilation.
Et si vous voulez voir la boxe personnellement?
Heureusement, vous n'avez pas besoin d'utiliser un désassembleur et d'entrer dans la jungle des codes. Nous avons les textes de l'ensemble du noyau de la plateforme .NET et beaucoup d'entre eux sont identiques en termes de .NET Framework CLR et CoreCLR. Vous pouvez cliquer sur les liens ci-dessous et voir immédiatement la mise en œuvre de la boxe:
Ici, la seule méthode est utilisée pour le déballage:
JIT_Unbox (..) , qui est un wrapper autour de JIT_Unbox_Helper (..) .
En outre, il est intéressant de noter que ( https://stackoverflow.com/questions/3743762/unboxing-does-not-create-a-copy-of-the-value-is-this-right ), le déballage ne signifie pas copier données au tas. La boxe signifie passer un pointeur vers le tas tout en testant la compatibilité des types. L'opcode IL suivant le déballage définira les actions avec cette adresse. Les données peuvent être copiées dans une variable locale ou la pile pour appeler une méthode. Sinon, nous aurions une double copie; d'abord lors de la copie du tas vers quelque part, puis de la copie vers l'emplacement de destination.
Des questions
Pourquoi .NET CLR ne peut pas faire de pooling pour la boxe elle-même?
Si nous parlons à un développeur Java, nous saurons deux choses:
- Tous les types de valeurs en Java sont encadrés, ce qui signifie qu'ils ne sont pas essentiellement des types de valeurs. Les entiers sont également encadrés.
- Pour des raisons d'optimisation, tous les entiers de -128 à 127 sont pris dans le pool d'objets.
Alors, pourquoi cela ne se produit-il pas dans .NET CLR pendant la boxe? C'est simple. Étant donné que nous pouvons modifier le contenu d'un type de valeur encadrée, nous pouvons effectuer les opérations suivantes:
object x = 1; x.GetType().GetField("m_value", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(x, 138); Console.WriteLine(x); // -> 138
Ou comme ça (C ++ / CLI):
void ChangeValue(Object^ obj) { Int32^ i = (Int32^)obj; *i = 138; }
Si nous nous occupions de la mise en commun, nous changerions tous ceux en application à 138, ce qui n'est pas bon.
Le suivant est l'essence des types de valeurs dans .NET. Ils traitent de la valeur, ce qui signifie qu'ils travaillent plus rapidement. La boxe est rare et l'ajout de numéros en boîte appartient au monde de la fantaisie et de la mauvaise architecture. Ce n'est pas du tout utile.
Pourquoi il n'est pas possible de faire de la boxe sur pile au lieu du tas, lorsque vous appelez une méthode qui prend un type d'objet, qui est en fait un type valeur?
Si la boxe du type valeur est effectuée sur la pile et que la référence ira au tas, la référence à l'intérieur de la méthode peut aller ailleurs, par exemple une méthode peut mettre la référence dans le champ d'une classe. La méthode s'arrêtera alors, et la méthode qui a fait de la boxe s'arrêtera également. Par conséquent, la référence pointera vers un espace mort sur la pile.
Pourquoi n'est-il pas possible d'utiliser Type de valeur comme champ?
Parfois, nous voulons utiliser une structure comme champ d'une autre structure qui utilise la première. Ou plus simple: utilisez la structure comme champ de structure. Ne me demandez pas pourquoi cela peut être utile. Ça ne peut pas. Si vous utilisez une structure comme champ ou par dépendance avec une autre structure, vous créez une récursivité, ce qui signifie une structure de taille infinie. Cependant, le .NET Framework a certains endroits où vous pouvez le faire. Un exemple est System.Char
, qui se contient :
public struct Char : IComparable, IConvertible { // Member Variables internal char m_value; //... }
Tous les types primitifs CLR sont conçus de cette façon. Nous, simples mortels, ne pouvons pas appliquer ce comportement. De plus, nous n'en avons pas besoin: cela est fait pour donner aux types primitifs un esprit de POO dans CLR.
Ce charper traduit du russe comme de la langue de l'auteur par des traducteurs professionnels . Vous pouvez nous aider à créer une version traduite de ce texte dans n'importe quelle autre langue, y compris le chinois ou l'allemand, en utilisant les versions russe et anglaise du texte comme source.
De plus, si vous voulez dire "merci", la meilleure façon que vous pouvez choisir est de nous donner une étoile sur github ou un référentiel de forking
https://github.com/sidristij/dotnetbook