example conversion and c# .net generics boxing unboxing

c# - conversion - Boxeo y unboxing con genéricos



boxing unboxing c# (6)

¿Por qué piensas en términos de WHERE están almacenados los valores? En C #, los tipos de valores se pueden almacenar tanto en pila como en montón, dependiendo de lo que elija CLR.

Donde los genéricos marcan la diferencia es WHAT se almacena en la colección. En el caso de ArrayList la colección contiene referencias a objetos encuadrados donde la List<int> contiene los valores int.

La forma .NET 1.0 de crear una colección de enteros (por ejemplo) era:

ArrayList list = new ArrayList(); list.Add(i); /* boxing */ int j = (int)list[0]; /* unboxing */

La pena de usar esto es la falta de seguridad y rendimiento del tipo debido al boxeo y al desempaquetado.

La forma de .NET 2.0 es usar genéricos:

List<int> list = new List<int>(); list.Add(i); int j = list[0];

El precio del boxeo (a mi entender) es la necesidad de crear un objeto en el montón, copiar el entero asignado a la pila al nuevo objeto y viceversa para el desempaquetado.

¿Cómo supera el uso de los genéricos esto? ¿El entero asignado por la pila permanece en la pila y se apunta desde el montón (supongo que este no es el caso debido a lo que sucederá cuando salga del alcance)? Parece que todavía hay una necesidad de copiarlo en otro lugar fuera de la pila.

¿Qué pasa en realidad?


Cuando se trata de colecciones, los genéricos hacen posible evitar el boxeo / unboxing utilizando matrices T[] reales internamente. List<T> por ejemplo, usa una matriz T[] para almacenar sus contenidos.

La matriz , por supuesto, es un tipo de referencia y, por lo tanto, está (en la versión actual del CLR, yada yada) almacenada en el montón. Pero como es un T[] y no un object[] , los elementos de la matriz se pueden almacenar "directamente": es decir, todavía están en el montón, pero están en el montón en la matriz en lugar de estar enmarcados y tener el conjunto contiene referencias a los cuadros.

Entonces, para List<int> , por ejemplo, lo que tendría en la matriz se vería así:

[ 1 2 3 ]

Compare esto con una ArrayList , que utiliza un object[] y, por lo tanto, "mirar" algo como esto:

[ *a *b *c ]

... donde *a , etc. son referencias a objetos (enteros encuadrados):

*a -> 1 *b -> 2 *c -> 3

Disculpen esas burdas ilustraciones; espero que sepas a que me refiero.


En .NET 1, cuando se llama al método Add :

  1. El espacio se asigna en el montón; se hace una nueva referencia
  2. El contenido de la variable i se copia en la referencia
  3. Una copia de la referencia se coloca al final de la lista

En .NET 2:

  1. Una copia de la variable i se pasa al método Add
  2. Una copia de esa copia se coloca al final de la lista

Sí, la variable i todavía se copia (después de todo, es un tipo de valor, y los tipos de valores siempre se copian, incluso si son solo parámetros del método). Pero no hay copia redundante hecha en el montón.


Generics permite que la matriz interna de la lista se tipee int[] lugar de efectivamente object[] , lo que requeriría boxeo.

Esto es lo que sucede sin genéricos:

  1. Usted llama a Add(1) .
  2. El entero 1 está encasillado en un objeto, lo que requiere que se construya un nuevo objeto en el montón.
  3. Este objeto se pasa a ArrayList.Add() .
  4. El objeto en caja se rellena en un object[] .

Aquí hay tres niveles de ArrayList indirecto: ArrayList -> object[] -> object -> int .

Con genéricos:

  1. Usted llama a Add(1) .
  2. El int 1 se pasa a List<int>.Add() .
  3. El int se rellena en un int[] .

Por lo tanto, solo hay dos niveles de direccionamiento indirecto: List<int> -> int[] -> int .

Algunas otras diferencias:

  • El método no genérico requerirá una suma de 8 o 12 bytes (un puntero, un int) para almacenar el valor, 4/8 en una asignación y 4 en la otra. Y esto probablemente será más debido a la alineación y el relleno. El método genérico requerirá solo 4 bytes de espacio en la matriz.
  • El método no genérico requiere asignar un int encuadrado; el método genérico no. Esto es más rápido y reduce la rotación de GC.
  • El método no genérico requiere moldes para extraer valores. Esto no es seguro y es un poco más lento.

Su confusión es el resultado de no entender cuál es la relación entre la pila, el montón y las variables. Esta es la forma correcta de pensarlo.

  • Una variable es una ubicación de almacenamiento que tiene un tipo.
  • La vida útil de una variable puede ser corta o larga. Por "corto" queremos decir "hasta que la función actual regrese o arroje" y por "largo" queremos decir "posiblemente más que eso".
  • Si el tipo de una variable es un tipo de referencia, el contenido de la variable es una referencia a una ubicación de almacenamiento de larga duración. Si el tipo de una variable es un tipo de valor, entonces el contenido de la variable es un valor.

Como detalle de implementación, una ubicación de almacenamiento que se garantiza que es de corta duración se puede asignar en la pila. Una ubicación de almacenamiento que puede ser de larga duración se asigna en el montón. Tenga en cuenta que esto no dice nada sobre "los tipos de valores siempre se asignan en la pila". Los tipos de valor no siempre se asignan en la pila:

int[] x = new int[10]; x[1] = 123;

x[1] es una ubicación de almacenamiento. Es longevo; podría vivir más tiempo que este método. Por lo tanto, debe estar en el montón. El hecho de que contenga un int es irrelevante.

Usted dice correctamente por qué un int en caja es caro:

El precio del boxeo es la necesidad de crear un objeto en el montón, copiar el entero asignado a la pila al nuevo objeto y viceversa para desempaquetar.

Donde va mal es decir "el entero asignado a la pila". No importa dónde se asignó el número entero. Lo que importa es que su almacenamiento contiene el número entero , en lugar de contener una referencia a una ubicación de montón . El precio es la necesidad de crear el objeto y hacer la copia; ese es el único costo que es relevante.

Entonces, ¿por qué una variable genérica no es costosa? Si tiene una variable de tipo T y T está construida para ser int, entonces tiene una variable de tipo int, punto. Una variable de tipo int es una ubicación de almacenamiento y contiene un int. Si esa ubicación de almacenamiento está en la pila o el montón es completamente irrelevante . Lo que es relevante es que la ubicación de almacenamiento contiene un int , en lugar de contener una referencia a algo en el montón . Dado que la ubicación de almacenamiento contiene un int, no tiene que asumir los costos de boxeo y desembalaje: asignando nuevo almacenamiento en el montón y copiando el int al nuevo almacenamiento.

¿Eso está claro?


Un ArrayList solo maneja el object tipo, por lo tanto, para utilizar esta clase se requiere el envío desde y hacia el object . En el caso de los tipos de valor, esta conversión implica boxeo y unboxing.

Cuando utiliza una lista genérica, el compilador genera un código especializado para ese tipo de valor, de modo que los valores reales se almacenan en la lista en lugar de una referencia a los objetos que contienen los valores. Por lo tanto, no se requiere boxeo.

El precio del boxeo (a mi entender) es la necesidad de crear un objeto en el montón, copiar el entero asignado a la pila al nuevo objeto y viceversa para el desempaquetado.

Creo que está asumiendo que los tipos de valores siempre se instancian en la pila. Este no es el caso: se pueden crear en el montón, en la pila o en los registros. Para obtener más información al respecto, consulte el artículo de Eric Lippert: The Truth About Value Types .