¿Por qué necesitamos boxeo y unboxing en C #?

325

¿Por qué necesitamos boxeo y unboxing en C #?

Sé lo que es el boxeo y el unboxing, pero no puedo comprender el uso real de este. ¿Por qué y dónde debo usarlo?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
Vaibhav Jain
fuente

Respuestas:

482

Por qué

Tener un sistema de tipos unificado y permitir que los tipos de valores tengan una representación completamente diferente de sus datos subyacentes de la forma en que los tipos de referencia representan sus datos subyacentes (por ejemplo, un intes solo un grupo de treinta y dos bits que es completamente diferente a una referencia tipo).

Piensa en esto, de esta manera. Tienes una variable ode tipo object. Y ahora tienes un inty quieres ponerlo o. oes una referencia a algo en alguna parte, y intenfáticamente no es una referencia a algo en alguna parte (después de todo, es solo un número). Entonces, lo que haces es esto: haces una nueva objectque puede almacenar inty luego le asignas una referencia a ese objeto o. A este proceso lo llamamos "boxeo".

Por lo tanto, si no le importa tener un sistema de tipos unificado (es decir, los tipos de referencia y los tipos de valores tienen representaciones muy diferentes y no desea una forma común de "representar" los dos), entonces no necesita boxeo. Si no le importa intrepresentar su valor subyacente (es decir, también intdebe ser tipos de referencia y simplemente almacenar una referencia a su valor subyacente), entonces no necesita boxeo.

¿Dónde debería usarlo?

Por ejemplo, el tipo de colección anterior ArrayListsolo come objects. Es decir, solo almacena referencias a algo que vive en algún lugar. Sin el boxeo, no se puede poner una inten tal colección. Pero con el boxeo, puedes.

Ahora, en los días de los genéricos, realmente no se necesita esto y, en general, se puede seguir alegremente sin pensar en el problema. Pero hay algunas advertencias a tener en cuenta:

Esto es correcto:

double e = 2.718281828459045;
int ee = (int)e;

Esto no es:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception

En su lugar, debes hacer esto:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;

Primero tenemos que desempaquetar explícitamente el double( (double)o) y luego transmitirlo a un int.

¿Cuál es el resultado de lo siguiente?

double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);

Piénselo por un segundo antes de pasar a la siguiente oración.

Si dijiste Truey Falsegenial! ¿Esperar lo? Esto se debe a que ==en los tipos de referencia se usa la igualdad de referencia que verifica si las referencias son iguales, no si los valores subyacentes son iguales. Este es un error peligrosamente fácil de cometer. Quizás aún más sutil

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);

también se imprimirá False!

Mejor decir:

Console.WriteLine(o1.Equals(o2));

que luego, afortunadamente, se imprimirá True.

Una última sutileza:

[struct|class] Point {
    public int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);

¿Cuál es el resultado? ¡Depende! Si Pointes a, structentonces la salida es 1pero si Pointes a, ¡ classentonces la salida es 2! Una conversión de boxeo hace una copia del valor que se está boxeando y explica la diferencia en el comportamiento.

jason
fuente
@ Jason ¿Quiere decir que si tenemos listas primitivas, no hay razón para usar un boxeo / unboxing?
Pacerier
No estoy seguro de lo que quieres decir con "lista primitiva".
Jason
3
¿Podría hablar sobre el impacto en el rendimiento de boxingy unboxing?
Kevin Meredith
@KevinMeredith hay una explicación básica sobre el rendimiento de las operaciones de boxeo y desempaquetado en msdn.microsoft.com/en-us/library/ms173196.aspx
InfZero
2
Excelente respuesta: mejor que la mayoría de las explicaciones que he leído en libros bien considerados.
FredM
59

En el marco .NET, hay dos especies de tipos: tipos de valor y tipos de referencia. Esto es relativamente común en los idiomas OO.

Una de las características importantes de los lenguajes orientados a objetos es la capacidad de manejar instancias de una manera independiente del tipo. Esto se conoce como polimorfismo . Dado que queremos aprovechar el polimorfismo, pero tenemos dos especies diferentes de tipos, tiene que haber alguna forma de unirlos para que podamos manejar uno u otro de la misma manera.

Ahora, en los viejos tiempos (1.0 de Microsoft.NET), no había este nuevo alboroto genérico. No podría escribir un método que tuviera un único argumento que pudiera servir un tipo de valor y un tipo de referencia. Eso es una violación del polimorfismo. Entonces, el boxeo fue adoptado como un medio para forzar un tipo de valor en un objeto.

Si esto no fuera posible, el marco estaría lleno de métodos y clases cuyo único propósito era aceptar las otras especies de tipo. No solo eso, sino que dado que los tipos de valor no comparten realmente un ancestro de tipo común, tendría que tener una sobrecarga de método diferente para cada tipo de valor (bit, byte, int16, int32, etc., etc.).

El boxeo evitó que esto sucediera. Y es por eso que los británicos celebran el día del boxeo.


fuente
1
Antes de los genéricos, el auto-boxeo era necesario para hacer muchas cosas; Sin embargo, dada la existencia de genéricos, si no fuera por la necesidad de mantener la compatibilidad con el código antiguo, creo que .net estaría mejor sin las conversiones de boxeo implícitas. Lanzar un tipo de valor como List<string>.Enumeratorpara IEnumerator<string>producir un objeto que se comporta principalmente como un tipo de clase, pero con un Equalsmétodo roto . Una mejor manera de fundición List<string>.Enumeratora IEnumerator<string>sería llamar a un operador de conversión personalizada, pero la existencia de un evita la conversión implícita de que.
supercat
42

La mejor manera de entender esto es observar los lenguajes de programación de nivel inferior en los que se basa C #.

En los lenguajes de nivel más bajo como C, todas las variables van a un lugar: The Stack. Cada vez que declaras una variable, va a la Pila. Solo pueden ser valores primitivos, como un bool, un byte, un int de 32 bits, un uint de 32 bits, etc. El Stack es simple y rápido. A medida que se agregan variables, simplemente van una encima de otra, por lo que la primera que declara se encuentra en digamos, 0x00, la siguiente en 0x01, la siguiente en 0x02 en RAM, etc. Además, las variables a menudo se direccionan previamente en la compilación. tiempo, por lo que su dirección se conoce incluso antes de ejecutar el programa.

En el siguiente nivel, como C ++, se introduce una segunda estructura de memoria llamada Heap. Todavía vive principalmente en la Pila, pero se pueden agregar entradas especiales llamadas Punteros a la Pila, que almacenan la dirección de memoria para el primer byte de un Objeto, y ese Objeto vive en el Montón. El Heap es un desastre y es algo costoso de mantener, porque a diferencia de las variables de Stack, no se acumulan linealmente hacia arriba y hacia abajo a medida que se ejecuta un programa. Pueden ir y venir sin una secuencia particular, y pueden crecer y encogerse.

Tratar con punteros es difícil. Son la causa de pérdidas de memoria, desbordamientos de búfer y frustración. C # al rescate.

En un nivel superior, C #, no necesita pensar en punteros: el marco .Net (escrito en C ++) piensa en estos por usted y se los presenta como referencias a objetos, y para el rendimiento, le permite almacenar valores más simples como bools, bytes e ints como tipos de valor. Debajo del capó, los Objetos y las cosas que crean instancias de una Clase van en el Montón costoso administrado por la memoria, mientras que los Tipos de valor van en la misma Pila que tenía en C de bajo nivel: súper rápido.

En aras de mantener la interacción entre estos 2 conceptos fundamentalmente diferentes de memoria (y estrategias de almacenamiento) simples desde la perspectiva de un codificador, los Tipos de valor se pueden encuadrar en cualquier momento. El boxeo hace que el valor se copie de la Pila, se coloque en un Objeto y se coloque en el Montón , una interacción más costosa pero fluida con el mundo de Referencia. Como señalan otras respuestas, esto ocurrirá cuando, por ejemplo, diga:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!

Una buena ilustración de la ventaja del boxeo es un cheque por nulo:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false

Nuestro objeto o es técnicamente una dirección en la pila que apunta a una copia de nuestro bool b, que se ha copiado en el montón. Podemos marcar o para nulo porque el bool ha sido encajonado y puesto allí.

En general, debe evitar el Boxeo a menos que lo necesite, por ejemplo, para pasar un int / bool / lo que sea como un objeto a un argumento. Hay algunas estructuras básicas en .Net que todavía exigen pasar Tipos de valor como objeto (y por lo tanto requieren Boxeo), pero en su mayor parte nunca debería necesitar Box.

Una lista no exhaustiva de estructuras históricas de C # que requieren Boxeo, que debe evitar:

  • El sistema de eventos resulta tener una condición de carrera en el uso ingenuo de él, y no admite asíncrono. Agregue el problema de Boxeo y probablemente debería evitarse. (Podría reemplazarlo, por ejemplo, con un sistema de eventos asíncrono que utiliza genéricos).

  • Los antiguos modelos Threading y Timer forzaron un Box en sus parámetros, pero han sido reemplazados por async / wait, que son mucho más limpios y más eficientes.

  • Las colecciones .Net 1.1 se basaron completamente en el boxeo, porque llegaron antes que los genéricos. Todavía están dando vueltas en System.Collections. En cualquier código nuevo, debe usar las Colecciones de System.Collections.Generic, que además de evitar el Boxeo también le brindan una mayor seguridad de escritura .

Debe evitar declarar o pasar sus Tipos de valor como objetos, a menos que tenga que lidiar con los problemas históricos anteriores que fuerzan el Boxeo, y desea evitar el impacto de rendimiento de Boxeo más adelante cuando sabe que de todos modos se va a Boxear.

Según la sugerencia de Mikael a continuación:

Hacer esto

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

No esta

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);

Actualizar

Esta respuesta originalmente sugirió que Int32, Bool, etc. causaran boxeo, cuando en realidad son alias simples para los Tipos de valor. Es decir, .Net tiene tipos como Bool, Int32, String y C # los alias a bool, int, string, sin ninguna diferencia funcional.

Chris Moschini
fuente
44
Me enseñaste lo que cien programadores y profesionales de TI no podían explicar en años, pero cámbialo para decir qué debes hacer en lugar de qué evitar, porque es un poco difícil de seguir ... las reglas básicas a menudo no funcionan 1 no debes hacer esto, en cambio haz esto
Mikael Puusaari
2
¡Esta respuesta debería haber sido marcada como RESPUESTA cientos de veces!
Pouyan
3
no hay "Int" en c #, hay int e Int32. Creo que está equivocado al afirmar que uno es un tipo de valor y el otro es un tipo de referencia que envuelve el tipo de valor. a menos que me equivoque, eso es cierto en Java, pero no en C #. En C #, los que aparecen en azul en el IDE son alias para su definición de estructura. Entonces: int = Int32, bool = Boolean, string = String. La razón para usar bool sobre Boolean es porque se sugiere como tal en las pautas y convenciones de diseño de MSDN. De lo contrario, me encanta esta respuesta. Pero rechazaré el voto hasta que demuestre que estoy equivocado o arregle eso en su respuesta.
Heriberto Lugo
2
Si declara una variable como int y otra como Int32, o bool y Boolean - haga clic derecho y vea la definición, terminará en la misma definición para una estructura.
Heriberto Lugo
2
@HeribertoLugo es correcto, la línea "Debe evitar declarar sus Tipos de valor como Bool en lugar de bool" está equivocada. Como OP señala, debe evitar declarar su bool (o Boolean, o cualquier otro tipo de valor) como Object. bool / Boolean, int / Int32, son solo alias entre C # y .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW
21

El boxeo no es realmente algo que usa, es algo que usa el tiempo de ejecución para que pueda manejar los tipos de referencia y valor de la misma manera cuando sea necesario. Por ejemplo, si usó una ArrayList para contener una lista de enteros, los enteros se encuadraron para encajar en las ranuras de tipo de objeto en la ArrayList.

Usando colecciones genéricas ahora, esto prácticamente desaparece. Si crea un List<int>, no se realiza ningún boxeo; List<int>puede contener los enteros directamente.

Rayo
fuente
Todavía necesita boxeo para cosas como el formato de cadena compuesta. Es posible que no lo vea con tanta frecuencia cuando usa genéricos, pero definitivamente todavía está allí.
Jeremy S
1
verdadero: también se muestra todo el tiempo en ADO.NET: los valores de los parámetros sql son todos 'objetos, sin importar el tipo de datos real'
Ray
11

Boxing y Unboxing se usan específicamente para tratar objetos de tipo valor como tipo de referencia; moviendo su valor real al montón administrado y accediendo a su valor por referencia.

Sin boxing y unboxing, nunca podría pasar los tipos de valor por referencia; y eso significa que no puede pasar tipos de valores como instancias de Object.

STW
fuente
sigue siendo una buena respuesta después de casi 10 años señor +1
snr
1
El paso por referencia de tipos numéricos existe en idiomas sin boxeo, y otros idiomas implementan el tratamiento de los tipos de valores como instancias de Object sin boxeo y mueven el valor al montón (por ejemplo, implementaciones de lenguajes dinámicos donde los punteros están alineados a los límites de 4 bytes usan los cuatro inferiores bits de referencias para indicar que el valor es un entero o símbolo en lugar de un objeto completo; dichos tipos de valor son inmutables y del mismo tamaño que un puntero).
Pete Kirkham
8

El último lugar donde tuve que desempaquetar algo fue cuando escribí un código que recuperaba algunos datos de una base de datos (no estaba usando LINQ to SQL , simplemente el viejo ADO.NET ):

int myIntValue = (int)reader["MyIntValue"];

Básicamente, si está trabajando con API anteriores antes de los genéricos, se encontrará con el boxeo. Aparte de eso, no es tan común.

BFree
fuente
4

Se requiere el boxeo, cuando tenemos una función que necesita un objeto como parámetro, pero tenemos diferentes tipos de valores que deben pasarse, en ese caso necesitamos convertir primero los tipos de valores en tipos de datos de objetos antes de pasarlos a la función.

No creo que sea cierto, intente esto en su lugar:

class Program
    {
        static void Main(string[] args)
        {
            int x = 4;
            test(x);
        }

        static void test(object o)
        {
            Console.WriteLine(o.ToString());
        }
    }

Eso funciona bien, no utilicé boxing / unboxing. (¿A menos que el compilador haga eso detrás de escena?)

Manoj
fuente
Esto se debe a que todo hereda de System.Object, y le está dando al método un objeto con información adicional, por lo que básicamente está llamando al método de prueba con lo que está esperando y cualquier cosa que pueda esperar, ya que no espera nada en particular. Mucho en .NET se hace detrás de escena, y la razón por la cual es un lenguaje muy simple de usar
Mikael Puusaari
1

En .net, cada instancia de Object, o cualquier tipo derivado de ella, incluye una estructura de datos que contiene información sobre su tipo. Los tipos de valores "reales" en .net no contienen dicha información. Para permitir que los datos en los tipos de valores sean manipulados por rutinas que esperan recibir tipos derivados de objetos, el sistema define automáticamente para cada tipo de valor un tipo de clase correspondiente con los mismos miembros y campos. El boxeo crea nuevas instancias de este tipo de clase, copiando los campos de una instancia de tipo de valor. Unboxing copia los campos de una instancia del tipo de clase a una instancia del tipo de valor. Todos los tipos de clase que se crean a partir de tipos de valor se derivan de la clase ValueType llamada irónicamente (que, a pesar de su nombre, es en realidad un tipo de referencia).

Super gato
fuente
0

Cuando un método solo toma un tipo de referencia como parámetro (por ejemplo, un método genérico restringido a ser una clase a través de la newrestricción), no podrá pasarle un tipo de referencia y tener que encajonarlo.

Esto también es cierto para cualquier métodos que toman objectcomo un parámetro - esto tiene que ser un tipo de referencia.

Oded
fuente
0

En general, generalmente querrá evitar el encajonamiento de sus tipos de valor.

Sin embargo, hay casos raros en los que esto es útil. Si necesita apuntar al marco 1.1, por ejemplo, no tendrá acceso a las colecciones genéricas. Cualquier uso de las colecciones en .NET 1.1 requeriría tratar su tipo de valor como un System.Object, lo que causa el boxing / unboxing.

Todavía hay casos para que esto sea útil en .NET 2.0+. Cada vez que desee aprovechar el hecho de que todos los tipos, incluidos los tipos de valor, se pueden tratar como un objeto directamente, es posible que deba usar el boxing / unboxing. Esto puede ser útil a veces, ya que le permite guardar cualquier tipo en una colección (mediante el uso de objetos en lugar de T en una colección genérica), pero en general, es mejor evitar esto, ya que está perdiendo la seguridad de los tipos. Sin embargo, el único caso en el que el boxeo ocurre con frecuencia es cuando usa Reflection: muchas de las llamadas en reflexión requerirán boxing / unboxing cuando trabaje con tipos de valor, ya que el tipo no se conoce de antemano.

Hunain
fuente
0

El boxeo es la conversión de un valor a un tipo de referencia con los datos en algún desplazamiento en un objeto en el montón.

En cuanto a lo que realmente hace el boxeo. Aquí hay unos ejemplos

Mono C ++

void* mono_object_unbox (MonoObject *obj)
 {    
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
 }

#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
    t result;       \
    MONO_ENTER_GC_UNSAFE;   \
    result = expr;      \
    MONO_EXIT_GC_UNSAFE;    \
    return result;

static inline gpointer
mono_object_get_data (MonoObject *o)
{
    return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}

#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)

typedef struct {
    MonoVTable *vtable;
    MonoThreadsSync *synchronisation;
} MonoObject;

Unboxing en Mono es un proceso de lanzar un puntero en un desplazamiento de 2 gpointers en el objeto (por ejemplo, 16 bytes). A gpointeres a void*. Esto tiene sentido cuando se mira la definición de, MonoObjectya que claramente es solo un encabezado para los datos.

C ++

Para boxear un valor en C ++, podría hacer algo como:

#include <iostream>
#define Object void*

template<class T> Object box(T j){
  return new T(j);
}

template<class T> T unbox(Object j){
  T temp = *(T*)j;
  delete j;
  return temp;
}

int main() {
  int j=2;
  Object o = box(j);
  int k = unbox<int>(o);
  std::cout << k;
}
Lewis Kelsey
fuente