Tipos de referencia .NET vs Tipos de valor. Parte 2


El tipo base del objeto y la implementación de interfaces. Boxeo


Parece que pasamos por el infierno y el apogeo y podemos concretar cualquier entrevista, incluso la del equipo .NET CLR. Sin embargo, no nos apresuremos a microsoft.com y busquemos vacantes. Ahora, debemos comprender cómo los tipos de valores heredan un objeto si no contienen una referencia a SyncBlockIndex, ni un puntero a una tabla de métodos virtuales. Esto explicará completamente nuestro sistema de tipos y todas las piezas de un rompecabezas encontrarán su lugar. Sin embargo, necesitaremos más de una oración.


Ahora, recordemos nuevamente cómo se asignan los tipos de valor en la memoria. Obtienen el lugar en la memoria justo donde están. Los tipos de referencia obtienen asignación en el montón de objetos pequeños y grandes. Siempre dan una referencia al lugar en el montón donde está el objeto. Cada tipo de valor tiene métodos como ToString, Equals y GetHashCode. Son virtuales y reemplazables, pero no permiten heredar un tipo de valor anulando métodos. Si los tipos de valor usaran métodos reemplazables, necesitarían una tabla de métodos virtuales para enrutar las llamadas. Esto llevaría a los problemas de pasar estructuras al mundo no administrado: campos adicionales irían allí. Como resultado, hay descripciones de métodos de tipo de valor en alguna parte, pero no puede acceder a ellos directamente a través de una tabla de métodos virtuales.


Esto puede traer la idea de que la falta de herencia es artificial


Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.

Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio github / sidristij / dotnetbook .

Esto puede traer la idea de que la falta de herencia es artificial:


  • hay herencia de un objeto, pero no directo;
  • hay ToString, Equals y GetHashCode dentro de un tipo base. En los tipos de valor, estos métodos tienen su propio comportamiento. Esto significa que los métodos se anulan en relación con un object ;
  • Además, si lanza un tipo a un object , tiene todo el derecho de llamar a ToString, Equals y GetHashCode;
  • Al llamar a un método de instancia para un tipo de valor, el método obtiene otra estructura que es una copia de un original. Eso significa que llamar a un método de instancia es como llamar a un método estático: Method(ref structInstance, newInternalFieldValue) . De hecho, esta llamada pasa this , sin embargo, con una excepción. Un JIT debe compilar el cuerpo de un método, por lo que sería innecesario compensar los campos de estructura, saltando sobre el puntero a una tabla de métodos virtuales, que no existe en la estructura. Existe para los tipos de valor en otro lugar .

Los tipos son diferentes en el comportamiento, pero esta diferencia no es tan grande en el nivel de implementación en el CLR. Hablaremos de eso un poco más tarde.


Escribamos la siguiente línea en nuestro programa:


 var obj = (object)10; 

Nos permitirá tratar con el número 10 usando una clase base. Esto se llama boxeo. Eso significa que tenemos un VMT para llamar a métodos virtuales como ToString (), Equals y GetHashCode. En realidad, el boxeo crea una copia de un tipo de valor, pero no un puntero a un original. Esto se debe a que podemos almacenar el valor original en todas partes: en la pila o como un campo de una clase. Si lo convertimos en un tipo de objeto, podemos almacenar una referencia a este valor todo el tiempo que queramos. Cuando ocurre el boxeo:


  • CLR asigna espacio en el montón para una estructura + SyncBlockIndex + VMT de un tipo de valor (para llamar a ToString, GetHashCode, Equals);
  • copia una instancia de un tipo de valor allí.

Ahora, tenemos una variante de referencia de un tipo de valor. Una estructura tiene absolutamente el mismo conjunto de campos del sistema que un tipo de referencia ,
convirtiéndose en un tipo de referencia completo después del boxeo. La estructura se convirtió en una clase. Llamémoslo un salto mortal .NET. Este es un nombre justo.


Solo observe lo que sucede si usa una estructura que implementa una interfaz que usa la misma interfaz.


 struct Foo : IBoo { int x; void Boo() { x = 666; } } IBoo boo = new Foo(); boo.Boo(); 

Cuando creamos la instancia de Foo, su valor va de hecho a la pila. Luego colocamos esta variable en una variable de tipo de interfaz y la estructura en una variable de tipo de referencia. Luego, hay boxeo y tenemos el tipo de objeto como salida. Pero es una variable de tipo de interfaz. Eso significa que necesitamos conversión de tipo. Entonces, la llamada ocurre de esta manera:


 IBoo boo = (IBoo)(box_to_object)new Foo(); boo.Boo(); 

Escribir dicho código no es efectivo. Tendrá que cambiar una copia en lugar de 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 primera vez que miramos el código, no tenemos que saber a qué nos enfrentamos en el código que no sea el nuestro y ver una interfaz de conversión a IBoo. Esto nos hace pensar que Foo es una clase y no una estructura. Entonces no hay división visual en las estructuras y clases, lo que nos hace pensar que
los resultados de la modificación de la interfaz deben entrar en foo, lo que no sucede porque boo es una copia de foo. Eso es engañoso. En mi opinión, este código debería recibir comentarios, para que otros desarrolladores puedan lidiar con él.


La segunda cosa se relaciona con los pensamientos previos de que podemos lanzar un tipo de un objeto a IBoo. Esta es otra prueba de que un tipo de valor en caja es una variante de referencia de un tipo de valor. O bien, todos los tipos en un sistema de tipos son tipos de referencia. Simplemente podemos trabajar con estructuras como con tipos de valor, pasando su valor por completo. Desreferenciar un puntero a un objeto como diría en el mundo de C ++.


Puede objetar que si fuera cierto, se vería así:


 var referenceToInteger = (IInt32)10; 

Obtendríamos no solo un objeto, sino una referencia escrita para un tipo de valor encuadrado. Destruiría toda la idea de los tipos de valor (es decir, la integridad de su valor) permitiendo una gran optimización, basada en sus propiedades. ¡Eliminemos esta idea!


 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(); } } 

Tenemos un análogo completo del boxeo. Sin embargo, podemos cambiar su contenido llamando a métodos de instancia. Estos cambios afectarán a todas las partes con referencia a esta estructura de datos.


 var typedBoxing = new Boxed<int> { Value = 10 }; var pureBoxing = (object)10; 

La primera variante no es muy atractiva. En lugar de lanzar un tipo, creamos tonterías. La segunda línea es mucho mejor, pero las dos líneas son casi idénticas. La única diferencia es que no hay limpieza de memoria con ceros durante el boxeo habitual después de asignar memoria en el montón. La estructura necesaria lleva la memoria de inmediato, mientras que la primera variante necesita limpieza. Esto hace que funcione más tiempo que el boxeo habitual en un 10%.


En cambio, podemos llamar a algunos métodos para nuestro valor en caja.


 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; 

Tenemos un nuevo instrumento. Pensemos qué podemos hacer con él.


  • Nuestro tipo Boxed<T> hace lo mismo que el tipo habitual: asigna memoria en el montón, pasa un valor allí y permite obtenerlo, haciendo una especie de unbox;
  • Si pierde una referencia a una estructura en caja, el GC la recopilará;
  • Sin embargo, ahora podemos trabajar con un tipo en caja, es decir, llamando a sus métodos;
  • Además, podemos reemplazar una instancia de un tipo de valor en SOH / LOH por otra. No podíamos hacerlo antes, ya que tendríamos que hacer unboxing, cambiar la estructura a otra y volver a boxear, dando una nueva referencia a los clientes.

El principal problema del boxeo es crear tráfico en la memoria. El tráfico de un número desconocido de objetos, cuya parte puede sobrevivir hasta la generación uno, donde tenemos problemas con la recolección de basura. Habrá mucha basura y podríamos haberla evitado. Pero cuando tenemos el tráfico de objetos de corta duración, la primera solución es la agrupación. Este es un final ideal de .NET somersault.


 var pool = new Pool<Boxed<Foo>>(maxCount:1000); var boxed = pool.Box(10); boxed.Value=70; // use boxed value here pool.Free(boxed); 

Ahora el boxeo puede funcionar utilizando un grupo, lo que elimina el tráfico de memoria durante el boxeo. Incluso podemos hacer que los objetos vuelvan a la vida en el método de finalización y volver a colocarse en el grupo. Esto puede ser útil cuando una estructura en caja va a un código asincrónico que no sea el suyo y no puede entender cuándo se volvió innecesario. En este caso, volverá a la agrupación durante el GC.


Vamos a concluir:


  • Si el boxeo es accidental y no debería suceder, no lo haga. Puede conducir a problemas con el rendimiento.
  • Si el boxeo es necesario para la arquitectura de un sistema, puede haber variantes. Si el tráfico de estructuras en caja es pequeño y casi invisible, puede usar el boxeo. Si el tráfico es visible, es posible que desee agrupar el boxeo, utilizando una de las soluciones indicadas anteriormente. Gasta algunos recursos, pero hace que GC funcione sin sobrecarga;

Finalmente, veamos un código totalmente poco práctico:


 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; } } 

El código usa una función pequeña, que puede obtener un puntero de una referencia a un objeto. La biblioteca está disponible en la dirección de github . Este ejemplo muestra que el boxeo habitual convierte int en un tipo de referencia escrito. Vamos
mira los pasos en el proceso:


  1. Hacer boxeo para un número entero.
  2. Obtener la dirección de un objeto obtenido (la dirección de Int32 VMT)
  3. Obtenga el VMT de un SimpleIntHolder
  4. Reemplace la VMT de un entero en caja por la VMT de una estructura.
  5. Convertir unboxing en un tipo de estructura
  6. Mostrar el valor del campo en la pantalla, obteniendo el Int32, que era
    en caja

Lo hago a través de la interfaz a propósito, ya que quiero mostrar que funcionará
de esa manera


Anulable \ <T>


Vale la pena mencionar sobre el comportamiento del boxeo con tipos de valores anulables. Esta característica de los tipos de valores anulables es muy atractiva ya que el boxeo de un tipo de valor que es una especie de nulo devuelve nulo.


 int? x = 5; int? y = null; var boxedX = (object)x; // -> 5 var boxedY = (object)y; // -> null 

Esto nos lleva a una conclusión peculiar: como nulo no tiene un tipo, el
La única forma de obtener un tipo, diferente del cuadro, es la siguiente:


 int? x = null; var pseudoBoxed = (object)x; double? y = (double?)pseudoBoxed; 

El código funciona solo porque puedes convertir un tipo a lo que quieras
con nulo


Profundizando en el boxeo


Como último comentario, me gustaría contarles sobre el tipo System.Enum . Lógicamente, este debería ser un tipo de valor, ya que es una enumeración habitual: alias de números a nombres en un lenguaje de programación. Sin embargo, System.Enum es un tipo de referencia. Todos los tipos de datos de enumeración, definidos en su campo, así como en .NET Framework, se heredan de System.Enum. Es un tipo de datos de clase. Además, es una clase abstracta, heredada de System.ValueType .


  [Serializable] [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { // ... } 

¿Significa que todas las enumeraciones se asignan en el SOH y cuando las usamos, sobrecargamos el montón y el GC? En realidad no, ya que solo los usamos. Luego, suponemos que hay un grupo de enumeraciones en algún lugar y solo obtenemos sus instancias. No otra vez Puede usar enumeraciones en estructuras durante el cálculo de referencias. Las enumeraciones son números habituales.


La verdad es que CLR piratea la estructura de tipo de datos al formarla si hay una enumeración que convierte una clase en un tipo de valor :


 // 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(); } } 

¿Por qué hacer esto? En particular, debido a la idea de herencia: hacer una enumeración personalizada, por ejemplo, debe especificar los nombres de los posibles valores. Sin embargo, es imposible heredar los tipos de valor. Por lo tanto, los desarrolladores lo diseñaron para ser un tipo de referencia que puede convertirlo en un tipo de valor cuando se compila.


¿Qué pasa si quieres ver el boxeo personalmente?


Afortunadamente, no tiene que usar un desensamblador y entrar en la jungla de códigos. Tenemos los textos de todo el núcleo de la plataforma .NET y muchos de ellos son idénticos en términos de .NET Framework CLR y CoreCLR. Puede hacer clic en los enlaces a continuación y ver la implementación del boxeo de inmediato:



Aquí, el único método se utiliza para unboxing:
JIT_Unbox (..) , que es un contenedor alrededor de JIT_Unbox_Helper (..) .


Además, es interesante que ( https://stackoverflow.com/questions/3743762/unboxing-does-not-create-a-copy-of-the-value-is-this-right ), unboxing no significa copiar datos al montón. El boxeo significa pasar un puntero al montón mientras se prueba la compatibilidad de los tipos. El código de operación de IL que sigue al desempaquetado definirá las acciones con esta dirección. Los datos pueden copiarse en una variable local o en la pila para llamar a un método. De lo contrario, tendríamos una doble copia; primero cuando se copia desde el montón a algún lugar, y luego se copia al lugar de destino.


Preguntas


¿Por qué .NET CLR no puede agrupar para el boxeo?


Si hablamos con cualquier desarrollador de Java, sabremos dos cosas:


  • Todos los tipos de valores en Java están encuadrados, lo que significa que no son esencialmente tipos de valores. Los enteros también están en caja.
  • Por razones de optimización, todos los enteros desde -128 hasta 127 se toman del conjunto de objetos.

Entonces, ¿por qué esto no sucede en .NET CLR durante el boxeo? Es simple Debido a que podemos cambiar el contenido de un tipo de valor encuadrado, podemos hacer lo siguiente:


 object x = 1; x.GetType().GetField("m_value", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(x, 138); Console.WriteLine(x); // -> 138 

O así (C ++ / CLI):


 void ChangeValue(Object^ obj) { Int32^ i = (Int32^)obj; *i = 138; } 

Si tratamos con la agrupación, entonces cambiaríamos todos los de la aplicación a 138, lo que no es bueno.


El siguiente es la esencia de los tipos de valor en .NET. Tratan con valor, lo que significa que trabajan más rápido. El boxeo es raro y la adición de números en caja pertenece al mundo de la fantasía y la mala arquitectura. Esto no es útil en absoluto.


¿Por qué no es posible hacer boxeo en la pila en lugar del montón, cuando se llama a un método que toma un tipo de objeto, que de hecho es un tipo de valor?


Si el cuadro de tipo de valor se realiza en la pila y la referencia irá al montón, la referencia dentro del método puede ir a otro lugar, por ejemplo, un método puede poner la referencia en el campo de una clase. El método se detendrá y el método que hizo el boxeo también se detendrá. Como resultado, la referencia apuntará a un espacio muerto en la pila.


¿Por qué no es posible usar el Tipo de valor como campo?


A veces queremos usar una estructura como un campo de otra estructura que usa la primera. O más simple: use la estructura como un campo de estructura. No me preguntes por qué esto puede ser útil. No puede. Si usa una estructura como su campo o mediante la dependencia con otra estructura, crea una recursión, lo que significa una estructura de tamaño infinito. Sin embargo, .NET Framework tiene algunos lugares donde puede hacerlo. Un ejemplo es System.Char , que se contiene a sí mismo :


 public struct Char : IComparable, IConvertible { // Member Variables internal char m_value; //... } 

Todos los tipos primitivos CLR están diseñados de esta manera. Nosotros, simples mortales, no podemos implementar este comportamiento. Además, no necesitamos esto: se hace para dar a los tipos primitivos un espíritu de OOP en CLR.


Este traductor traducido del ruso como del idioma del autor por traductores profesionales . Puede ayudarnos a crear una versión traducida de este texto a cualquier otro idioma, incluido el chino o el alemán, utilizando las versiones de texto en ruso e inglés como fuente.

Además, si quiere decir "gracias", la mejor manera de elegir es dándonos una estrella en github o bifurcando repositorio https://github.com/sidristij/dotnetbook

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


All Articles