Este artículo le mostrará los conceptos básicos de los tipos internos, como, por supuesto, un ejemplo en el que la memoria para el tipo de referencia se asignará completamente en la pila (esto se debe a que soy un programador de pila completa).

Descargo de responsabilidad
Este artículo no contiene material que deba usarse en proyectos reales. Es simplemente una extensión de los límites en los que se percibe un lenguaje de programación.
Antes de continuar con la historia, le recomiendo que lea la primera publicación sobre
StructLayout , porque hay un ejemplo que se utilizará en este artículo (Sin embargo, como siempre).
Prehistoria
Comenzando a escribir código para este artículo, quería hacer algo interesante usando lenguaje ensamblador. Quería romper de alguna manera el modelo de ejecución estándar y obtener un resultado realmente inusual. Y recordando con qué frecuencia la gente dice que el tipo de referencia difiere de los tipos de valor en que los primeros están ubicados en el montón y los segundos están en la pila, decidí usar un ensamblador para mostrar que el tipo de referencia puede vivir en el apilar Sin embargo, comencé a encontrarme con todo tipo de problemas, por ejemplo, devolviendo la dirección y su presentación como un enlace administrado (todavía estoy trabajando en ello). Entonces comencé a hacer trampa y hacer algo que no funciona en lenguaje ensamblador, en C #. Y al final, no hubo ensamblador en absoluto.
Lea también la recomendación: si está familiarizado con el diseño de los tipos de referencia, le recomiendo omitir la teoría sobre ellos (solo se darán los conceptos básicos, nada interesante).
Un poco sobre los aspectos internos de los tipos (para el marco anterior, ahora se cambian algunas compensaciones, pero el esquema general es el mismo)
Me gustaría recordar que la división de la memoria en una pila y un montón se produce en el nivel .NET, y esta división es puramente lógica; físicamente no hay diferencia entre las áreas de memoria debajo del montón y la pila. La diferencia en productividad es proporcionada solo por diferentes algoritmos de trabajo con estas dos áreas.
Entonces, ¿cómo asignar memoria en la pila? Para empezar, comprendamos cómo se organiza este tipo de referencia misterioso y qué tiene, ese tipo de valor no tiene.
Entonces, considere el ejemplo más simple con la clase Empleado.
Código de empleadopublic 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..."); } }
Y echemos un vistazo a cómo se presenta en la memoria.
Esta clase se considera en el ejemplo de un sistema de 32 bits.

Por lo tanto, además de la memoria para los campos, tenemos dos campos ocultos más: el índice del bloque de sincronización (título de la palabra del encabezado del objeto en la imagen) y la dirección de la tabla de métodos.
El primer campo (el índice del bloque de sincronización) no nos interesa realmente. Al colocar el tipo, decidí omitirlo. Hice esto por dos razones:
- Soy muy vago (no dije que las razones sean razonables)
- Para la operación básica del objeto, este campo no es obligatorio.
Pero como ya hemos comenzado a hablar, creo que es correcto decir algunas palabras sobre este campo. Se utiliza para diferentes propósitos (código hash, sincronización). Más bien, el campo en sí mismo es simplemente un índice de uno de los bloques de sincronización asociados con el objeto dado. Los bloques mismos se encuentran en la tabla de bloques de sincronización (algo así como una matriz global). Crear un bloque de este tipo es una operación bastante grande, por lo que no se crea si no es necesario. Además, cuando se usan bloqueos delgados, el identificador del hilo que recibió el bloqueo (en lugar del índice) se escribirá allí.
El segundo campo es mucho más importante para nosotros. Gracias a la tabla de métodos de tipo, es posible una herramienta tan poderosa como el polimorfismo (que, por cierto, las estructuras, los reyes de la pila, no poseen).
Suponga que la clase Employee implementa adicionalmente tres interfaces: IComparable, IDisposable e ICloneable.
Entonces la tabla de métodos se verá más o menos así.

La imagen es genial, todo se muestra y todo está claro. En resumen, el método virtual no se llama directamente por dirección, sino por el desplazamiento en la tabla de métodos. En la jerarquía, los mismos métodos virtuales se ubicarán en el mismo desplazamiento en la tabla de métodos. Es decir, en la clase base llamamos al método por desplazamiento, sin saber qué tipo de tabla de métodos se utilizará, pero sabiendo que este desplazamiento será el método más relevante para el tipo de tiempo de ejecución.
También vale la pena recordar que la referencia del objeto apunta solo al puntero de la tabla del método.
Ejemplo muy esperado
Comencemos con clases que nos ayudarán en nuestro objetivo. Usando StructLayout (realmente lo intenté sin él, pero no funcionó), escribí mapeadores simples: punteros a tipos administrados y viceversa. Obtener un puntero de un enlace administrado es bastante fácil, pero la transformación inversa me causó dificultades y, sin pensarlo dos veces, apliqué mi atributo favorito. Para mantener el código en una clave, hecho en 2 direcciones de una manera.
Código de los mapeadores // 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; }
Primero, escribimos un método que lleva un puntero a alguna memoria (no necesariamente en la pila, por cierto) y configura el tipo.
Por la simplicidad de encontrar la dirección de la tabla de métodos, creo un tipo en el montón. Estoy seguro de que la tabla de métodos se puede encontrar de otras maneras, pero no me propuse el objetivo de optimizar este código, fue más interesante para mí hacerlo comprensible. Además, utilizando los convertidores descritos anteriormente, obtenemos un puntero al tipo creado.
Este puntero apunta exactamente a la tabla de métodos. Por lo tanto, es suficiente simplemente obtener los contenidos de la memoria a la que apunta. Esta será la dirección de la tabla de métodos.
Y dado que el puntero que se nos pasa es una especie de referencia de objeto, también debemos escribir la dirección de la tabla de métodos exactamente donde apunta.
En realidad, eso es todo. De repente, ¿verdad? Ahora nuestro tipo está listo. Pinocho, quien nos asignó memoria, se encargará de inicializar los campos él mismo.
Solo queda usar nuestro ultramega lanzador para convertir el puntero en un enlace administrado.
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; } }
Ahora tenemos un enlace en la pila que apunta a la misma pila, donde según todas las leyes de los tipos de referencia (bueno, casi) yace un objeto construido a partir de tierra negra y palos. El polimorfismo está disponible.
Debe entenderse que si pasa este enlace fuera del método, luego de regresar de él, obtendremos algo poco claro. Sobre las llamadas de métodos virtuales y el habla no puede ser, se producirá la excepción. Los métodos normales se llaman directamente, el código solo tendrá direcciones para métodos reales, por lo que funcionarán. Y en lugar de los campos habrá ... y nadie sabe qué habrá allí.
Dado que es imposible usar un método separado para la inicialización en la pila (dado que el marco de la pila se sobrescribirá después de regresar del método), el método que desea aplicar el tipo en la pila debe asignar memoria. Estrictamente hablando, hay algunas formas de hacerlo. Pero lo más adecuado para nosotros es
stackalloc . La palabra clave perfecta para nuestros propósitos. Desafortunadamente, trae lo
inseguro en el código. Antes de eso, había una idea de usar Span para estos fines y prescindir de un código inseguro. En el código inseguro no hay nada malo, pero como en todas partes, no es una bala de plata y tiene sus propias áreas de aplicación.
Luego, después de recibir el puntero a la memoria en la pila actual, pasamos este puntero al método que compone el tipo en partes. Eso fue todo lo que escuchó, bien hecho.
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(); } }
No debe usarlo en proyectos reales, el método de asignación de memoria en la pila usa el nuevo T (), que a su vez usa la reflexión para crear un tipo en el montón. Entonces, este método será más lento que la creación habitual del tipo de veces bien, en 40-50. Además no es multiplataforma.
Aquí puedes encontrar todo el proyecto.
Fuente: en la guía teórica, se utilizaron ejemplos del libro Sasha Goldstein - Pro .NET Performace