Cet article vous montrera les bases des types internes, comme bien sûr un exemple dans lequel la mémoire pour le type de référence sera allouée complètement sur la pile (c'est parce que je suis un programmeur à pile complète).

Clause de non-responsabilité
Cet article ne contient pas de matériel qui devrait être utilisé dans des projets réels. Il s'agit simplement d'une extension des frontières dans lesquelles un langage de programmation est perçu.
Avant de poursuivre avec l'histoire, je vous recommande fortement de lire le premier post sur
StructLayout , car il y a un exemple qui sera utilisé dans cet article (Cependant, comme toujours).
Préhistoire
Commençant à écrire du code pour cet article, je voulais faire quelque chose d'intéressant en utilisant le langage d'assemblage. Je voulais en quelque sorte casser le modèle d'exécution standard et obtenir un résultat vraiment inhabituel. Et en me souvenant de la fréquence à laquelle les gens disent que le type de référence diffère des types de valeur en ce que les premiers sont situés sur le tas et les seconds sur la pile, j'ai décidé d'utiliser un assembleur pour montrer que le type de référence peut vivre sur le pile. Cependant, j'ai commencé à rencontrer toutes sortes de problèmes, par exemple en renvoyant l'adresse et sa présentation sous forme de lien géré (j'y travaille toujours). J'ai donc commencé à tricher et à faire quelque chose qui ne fonctionne pas en langage assembleur, en C #. Et à la fin, il n'y avait pas du tout d'assembleur.
Lisez également la recommandation - si vous êtes familier avec la disposition des types de référence, je vous recommande de sauter la théorie à leur sujet (seules les bases seront données, rien d'intéressant).
Un peu sur les internes des types (pour l'ancien framework, maintenant certains décalages sont modifiés, mais le schéma global est le même)
Je voudrais rappeler que la division de la mémoire en une pile et un tas se produit au niveau .NET, et cette division est purement logique; il n'y a physiquement aucune différence entre les zones de mémoire sous le tas et la pile. La différence de productivité n'est fournie que par différents algorithmes de travail avec ces deux domaines.
Alors, comment allouer de la mémoire sur la pile? Pour commencer, comprenons comment ce mystérieux type de référence est organisé et ce qu'il a, ce type de valeur n'a pas.
Considérez donc l'exemple le plus simple avec la classe Employee.
Code employépublic class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } }
Et voyons comment il est présenté en mémoire.
Cette classe est considérée sur l'exemple d'un système 32 bits.

Ainsi, en plus de la mémoire des champs, nous avons deux autres champs cachés - l'index du bloc de synchronisation (titre du mot d'en-tête d'objet dans l'image) et l'adresse de la table de méthode.
Le premier champ (l'index du bloc de synchronisation) ne nous intéresse pas vraiment. En plaçant le type, j'ai décidé de le sauter. Je l'ai fait pour deux raisons:
- Je suis très paresseux (je n'ai pas dit que les raisons seraient raisonnables)
- Pour le fonctionnement de base de l'objet, ce champ n'est pas obligatoire.
Mais comme nous avons déjà commencé à parler, je pense qu'il est juste de dire quelques mots sur ce domaine. Il est utilisé à différentes fins (code de hachage, synchronisation). Au contraire, le champ lui-même est simplement un index de l'un des blocs de synchronisation associés à l'objet donné. Les blocs eux-mêmes sont situés dans la table des blocs de synchronisation (quelque chose comme un tableau global). La création d'un tel bloc est une opération assez importante, donc il n'est pas créé s'il n'est pas nécessaire. De plus, lors de l'utilisation de verrous fins, l'identifiant du thread qui a reçu le verrou (au lieu de l'index) y sera écrit.
Le deuxième domaine est beaucoup plus important pour nous. Grâce à la table des méthodes de types, un outil aussi puissant que le polymorphisme est possible (qui, soit dit en passant, les structures, empilent les rois, ne possèdent pas).
Supposons que la classe Employee implémente en outre trois interfaces: IComparable, IDisposable et ICloneable.
Ensuite, le tableau des méthodes ressemblera à ceci.

L'image est très cool, tout est montré et tout est net. Pour résumer, la méthode virtuelle n'est pas appelée directement par adresse, mais par le décalage dans la table des méthodes. Dans la hiérarchie, les mêmes méthodes virtuelles seront situées au même décalage dans la table des méthodes. Autrement dit, sur la classe de base, nous appelons la méthode par décalage, ne sachant pas quel type de table de méthodes sera utilisé, mais sachant que ce décalage sera la méthode la plus pertinente pour le type d'exécution.
Il convient également de se rappeler que la référence d'objet pointe uniquement vers le pointeur de la table des méthodes.
Exemple tant attendu
Commençons par des cours qui nous aideront dans notre objectif. En utilisant StructLayout (j'ai vraiment essayé sans, mais cela n'a pas fonctionné), j'ai écrit des mappeurs simples - des pointeurs vers des types gérés et inversement. Obtenir un pointeur à partir d'un lien géré est assez facile, mais la transformation inverse m'a causé des difficultés et, sans y réfléchir à deux fois, j'ai appliqué mon attribut préféré. Pour garder le code dans une clé, faite dans 2 directions dans un sens.
Code des cartographes // Provides the signatures we need public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // Provides the logic we need public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; }
Tout d'abord, nous écrivons une méthode qui prend un pointeur vers une certaine mémoire (pas nécessairement sur la pile, soit dit en passant) et configure le type.
Pour la simplicité de trouver l'adresse de la table de méthode, je crée un type sur le tas. Je suis sûr que le tableau des méthodes peut être trouvé autrement, mais je ne me suis pas fixé pour objectif d'optimiser ce code, il était plus intéressant pour moi de le rendre compréhensible. De plus, en utilisant les convertisseurs décrits précédemment, nous obtenons un pointeur sur le type créé.
Ce pointeur pointe exactement vers la table des méthodes. Par conséquent, il suffit d'obtenir simplement le contenu de la mémoire vers laquelle il pointe. Ce sera l'adresse de la table des méthodes.
Et puisque le pointeur qui nous est transmis est une sorte de référence d'objet, nous devons également écrire l'adresse de la table de méthode exactement là où elle pointe.
En fait, c'est tout. Soudain, non? Maintenant, notre type est prêt. Pinocchio, qui nous a attribué de la mémoire, se chargera lui-même de l'initialisation des champs.
Il ne reste plus qu'à utiliser notre roulette ultra-méga pour convertir le pointeur en lien géré.
public class StackInitializer { public static unsafe T InitializeOnStack<T>(int* pointer) where T : new() { T r = new T(); var caster = new PointerCaster().Caster; int* ptr = caster.GetPointerByManagedReference(r); pointer[0] = ptr[0]; T reference = caster.GetManagedReferenceByPointer<T>(pointer); return reference; } }
Nous avons maintenant un lien sur la pile qui pointe vers la même pile, où selon toutes les lois des types de référence (enfin, presque) se trouve un objet construit à partir de terre noire et de bâtons. Le polymorphisme est disponible.
Il faut comprendre que si vous passez ce lien en dehors de la méthode, puis après son retour, nous obtiendrons quelque chose de peu clair. À propos des appels de méthodes virtuelles et de la parole ne peut pas être, l'exception se produira. Les méthodes normales sont appelées directement, le code n'aura que des adresses pour les méthodes réelles, donc elles fonctionneront. Et à la place des champs seront ... et personne ne sait ce qui sera là.
Puisqu'il est impossible d'utiliser une méthode distincte pour l'initialisation sur la pile (puisque le cadre de pile sera écrasé après son retour de la méthode), la méthode qui veut appliquer le type sur la pile doit allouer de la mémoire. À strictement parler, il existe plusieurs façons de procéder. Mais le plus approprié pour nous est
stackalloc . Juste le mot-clé parfait pour nos besoins. Malheureusement, cela apporte le
dangereux dans le code. Avant cela, il y avait une idée d'utiliser Span à ces fins et de se passer de code dangereux. Dans le code dangereux, il n'y a rien de mal, mais comme partout, ce n'est pas une solution miracle et a ses propres domaines d'application.
Ensuite, après avoir reçu le pointeur vers la mémoire de la pile actuelle, nous passons ce pointeur à la méthode qui compose le type en plusieurs parties. C'est tout ce qui a écouté - bravo.
unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } }
Vous ne devez pas l'utiliser dans des projets réels, la méthode d'allocation de mémoire sur la pile utilise un nouveau T (), qui à son tour utilise la réflexion pour créer un type sur le tas! Cette méthode sera donc plus lente que la création habituelle du type de fois bien, en 40-50. De plus ce n'est pas multiplateforme.
Ici vous pouvez trouver l'ensemble du projet.
Source: dans le guide théorique, des exemples du livre Sasha Goldstein - Pro .NET Performace ont été utilisés