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

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

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

short s = 25; object objshort = s; //Boxing short anothershort = (short)objshort; //Unboxing 

Por qué

Tener un sistema de tipo 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 int es solo un cubo de treinta y dos bits que es completamente diferente a tipo de referencia).

Piensa en esto, de esta manera. Usted tiene una variable o de tipo object . Y ahora tienes un int y quieres ponerlo en o . o es una referencia a algo en alguna parte, y el int es enfáticamente una referencia a algo en alguna parte (después de todo, es solo un número). Entonces, lo que haces es esto: creas un nuevo object que puede almacenar el int y luego asignas una referencia a ese objeto a o . Llamamos a este proceso “boxeo”.

Entonces, si no te importa tener un sistema de tipo unificado (es decir, los tipos de referencia y los tipos de valores tienen representaciones muy diferentes y no quieres una forma común de “representar” los dos), entonces no necesitas el boxeo. Si no te importa que int represente su valor subyacente (es decir, si tienes tipos de referencia int también y simplemente almacena una referencia a su valor subyacente) entonces no necesitas boxeo.

donde debería usarlo

Por ejemplo, el antiguo tipo de colección ArrayList solo come object s. Es decir, solo almacena referencias a algunas cosas que viven en alguna parte. Sin boxeo no puede poner un int en dicha colección. Pero con el boxeo, puedes.

Ahora, en los días de los generics, realmente no necesita esto y generalmente puede ir 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 cambio, debes hacer esto:

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

Primero tenemos que destrabar explícitamente el double ( (double)o ) y luego convertirlo en 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 un segundo antes de pasar a la siguiente oración.

¡Si dijeras True y False genial! ¿Esperar lo? Eso es porque == en los tipos de referencia utiliza referencia-igualdad, que verifica si las referencias son iguales, no si los valores subyacentes son iguales. Este es un error peligrosamente fácil de hacer. 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, 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; px = 2; Console.WriteLine(((Point)o).x); 

¿Cuál es el resultado? ¡Depende! Si Point es una struct , la salida es 1 pero si Point es una class , ¡la salida es 2 ! Una conversión de boxeo hace que una copia del valor esté en un recuadro explicando la diferencia en el comportamiento.

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

Una de las características importantes de los lenguajes orientados a objetos es la capacidad de manejar las instancias de una manera independiente del tipo. Esto se conoce como polymorphism . Como queremos aprovechar el polymorphism, pero tenemos dos especies diferentes de tipos, tiene que haber alguna manera 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 esta novela de genios hullabaloo. No se podía escribir un método que tuviera un solo argumento que pudiera dar servicio a un tipo de valor y a un tipo de referencia. Eso es una violación del polymorphism. 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 plagado de métodos y clases cuyo único propósito era aceptar las otras especies de tipo. No solo eso, sino que los tipos de valores realmente no comparten un ancestro de tipo común, tendrías 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.

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

En los idiomas de nivel más bajo como C, todas las variables van en un solo 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. The Stack es simple y rápido. A medida que se agregan variables, simplemente van una encima de la otra, de modo que la primera que declara se ubica en decir, 0x00, la siguiente en 0x01, la siguiente en 0x02 en RAM, etc. Además, las variables a menudo se direccionan previamente en comstackción- hora, por lo que su dirección es conocida incluso antes de ejecutar el progtwig.

En el siguiente nivel, como C ++, se introduce una segunda estructura de memoria llamada Heap. Aún vive en su mayoría en la Pila, pero se pueden agregar a la Pila apuntes especiales llamados Punteros , que almacenan la dirección de memoria para el primer byte de un Objeto, y ese Objeto vive en el Heap. El Heap es un poco complicado y 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 progtwig. Pueden ir y venir en ninguna secuencia particular, y pueden crecer y encogerse.

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

En un nivel superior, C #, no necesita pensar en punteros: el .Net Framework (escrito en C ++) piensa en esto por usted y se lo presenta como Referencias a Objetos, y para el rendimiento, le permite almacenar valores más simples. como bools, bytes y ints como Value Types. Debajo del capó, Objetos y cosas que crean una instancia de una Clase en el costoso, Compartido Administrado por Memoria, mientras que los Tipos de Valor van en la misma Pila que tenías en el nivel bajo C: súper rápido.

Con el fin de mantener la interacción entre estos 2 conceptos fundamentalmente diferentes de la memoria (y las estrategias para el almacenamiento) simples desde la perspectiva de un codificador, los Tipos de valores se pueden encasillar en cualquier momento. El boxeo hace que el valor se copie de la stack, se coloque en un objeto y se coloque en el montón : una interacción más fluida pero fluida con el mundo de referencia. Como otras respuestas señalan, esto ocurrirá cuando, por ejemplo, digas:

 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 para nulo:

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

Nuestro objective es técnicamente una dirección en la Pila que apunta a una copia de nuestro booleano, que ha sido copiada al Heap. Podemos verificar o por nulo porque el bool ha sido encerrado y puesto allí.

En general, debes evitar el boxeo a menos que lo necesites, 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 demandan pasar Tipos de valores como objeto (y así requerir Boxeo), pero en su mayoría nunca debería necesitar Box.

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

  • El sistema de Evento tiene una Condición de Carrera con un uso ingenuo y no admite la sincronización. Agregue el problema del boxeo y probablemente debería evitarse. (Podría reemplazarlo, por ejemplo, con un sistema de eventos asíncronos que use generics).

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

  • Las colecciones de .Net 1.1 se basaron por completo en el boxeo porque llegaron antes que los generics. Estos 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 proporcionan una mayor seguridad de tipo .

Debes evitar declarar o pasar tus Tipos de valores como objetos, a menos que tengas que lidiar con los problemas históricos anteriores que fuerzan al Boxeo, y quieres evitar el impacto en el rendimiento de Boxeo más tarde cuando sabes que va a ser Boxed de todos modos.

La sugerencia de Per Mikael a continuación:

Hacer esto

 using System.Collections.Generic; var employeeCount = 5; var list = new List(10); 

No esta

 using System.Collections; Int32 employeeCount = 5; var list = new ArrayList(10); 

Actualizar

Esta respuesta originalmente sugería Int32, Bool, etc. causa del boxeo, cuando en realidad son alias simples para los Tipos de valores. Es decir, .Net tiene tipos como Bool, Int32, String y C # los alíalos a bool, int, cadena, sin ninguna diferencia funcional.

El boxeo no es algo que uses, es algo que utiliza el tiempo de ejecución para que puedas manejar los tipos de referencia y valor de la misma manera cuando sea necesario. Por ejemplo, si utilizó una ArrayList para contener una lista de enteros, los enteros se encasillaron para que queparan en las ranuras de tipo de objeto en ArrayList.

Usando colecciones genéricas ahora, esto prácticamente desaparece. Si crea una List , no hay boxeo hecho – la List puede contener los enteros directamente.

Boxeo 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 boxeo y unboxing nunca podría pasar los tipos de valor por referencia; y eso significa que no podría pasar los tipos de valores como instancias de Object.

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

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

Básicamente, si trabajas con API más antiguas antes de los generics, te encontrarás con el boxeo. Aparte de eso, no es tan común.

Se requiere 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 primero tenemos que convertir tipos de valores a tipos de datos de objetos antes de pasarlos a la función.

No creo que eso 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 usé boxeo / unboxing. (A menos que el comstackdor haga eso detrás de escena?)

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 tal información. Para permitir que los datos en 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 una nueva instancia 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 clases que se crean a partir de los tipos de valores se derivan de la irónicamente llamada clase ValueType (que, a pesar de su nombre, es en realidad un tipo de referencia).

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

Esto también es cierto para cualquier método que tome el object como parámetro; este tendrá que ser un tipo de referencia.

En general, normalmente querrá evitar el uso de sus tipos de valores en el boxeo.

Sin embargo, hay casos raros donde esto es útil. Si necesita orientar el 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, que causa el boxeo / 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 necesite utilizar el boxeo / 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 seguridad de tipo. Sin embargo, el único caso donde el boxeo ocurre con frecuencia es cuando usas Reflection: muchas de las llamadas en reflection requerirán boxeo / unboxing cuando trabajes con tipos de valores, ya que el tipo no se conoce de antemano.