Crédito de la foto: Amöbe
Un efecto importante a tener en cuenta a la hora de usar los tipos por valor, es el conocido como Boxing. Éste consiste en que el compilador se ve forzado a convertir un tipo por valor en un tipo por referencia, y por tanto a almacenarlo en el montón en lugar de en la pila.
Nota: si no tienes claros los conceptos de tipo por valor, tipo por referencia, pila y montón, lee este artículo antes de continuar.
¿Por qué puede ocurrir esto?
Bueno, pues por muchos motivos, pero los más comunes son dos:
1.- Conversión forzada a tipo por referencia
Que hagamos una conversión forzada de un tipo por valor en uno por referencia en nuestro código. Por ejemplo:
int num = 1;
object obj = num;
De acuerdo, no es un código muy útil ni muy razonable, pero sirve para ilustrar la idea.
Lo que hacemos es asignar un tipo por valor a una variable pensada para albergar un tipo por referencia compatible. En este caso object es la clase base de todas las demás, así que se le puede asignar cualquier cosa a la variable obj.
En este ejemplo dado que num es un tipo por valor y obj es un tipo por referencia, para poder hacer la asignación, el compilador se ve forzado a convertir uno en el otro. Es decir, en “envolver” al tipo básico en un tipo por referencia y colocarlo en el montón. Lo que tendríamos al final es un nuevo objeto en el montón que albergaría el valor original (1 en este caso), y en la pila habría una referencia a este objeto.
A esta operación se le llama operación de Boxing, porque es como si envolviésemos al pobre valor en una caja para poder llevarlo cuidadosamente al montón y referenciarlo desde la pila.
A la operación contraria se le llama UnBoxing, y se produce cuando el tipo por referencia se convierte o se asigna a un tipo por valor de nuevo. Siguiendo con el código anterior, forzaríamos un unboxing con una línea así:
int num2 = (int) o;
Ahora, al forzar la conversión del objeto (tipo por referencia) en un tipo por valor (un número entero), provocamos el deshacer lo anterior.
¿Para qué querríamos hacer algo como esto? Pues por ejemplo porque tenemos que llamar a una función o clase que acepta objetos como argumentos, pero necesitamos pasarle tipos por valor.
Hoy en día como existen los genéricos no es tan habitual, pero anteriormente había muchas clases especializadas que podían trabajar con varios tipos de datos diferentes y solo aceptaban object como argumento (mira la colección ArrayList, por ejemplo).
2.- Usar un tipo por valor como si fuera por referencia
Este tipo de boxing es más sutil y puede pasar inadvertido a muchos programadores.
Como sabemos todos los objetos de .NET heredan de la clase Object. Esta clase ofrece una serie de métodos básicos que todos los objetos de .NET tienen, ya que heredan de ésta. Así, por ejemplo, el método ToString() está definido en Object y permite obtener una representación en formato texto de un objeto cualquiera. Además dispone también de un método GetType() que nos devuelve el tipo del objeto actual.
Por lo tanto podemos escribir algo como esto sin ningún problema:
int num = 1;
string s = num.ToString();
Console.WriteLine(num.GetType());
O sea, llamamos a ToString() para obtener el número como una cadena, y también llamamos a GetType() para, en este caso, mostrar por la consola el tipo del número (que es System.Int32, que es la estructura que lo representa).
Al ejecutar un código tan sencillo como este, veamos qué ocurre por debajo. Para ello lo que he hecho es compilarlo y descompilarlo usando la herramienta ILDASM.exe (que encontrarás en "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools" si tienes instalado Visual Studio).
Este es el aspecto del programa anterior en lenguaje intermedio:
Aquí debemos fijarnos en las dos instrucciones recuadradas en rojo en la figura anterior.
La primera se corresponde a la llamada a ToString(), y la segunda a la llamada a GetType(). Fijémonos antes de nada en esta última.
La instrucción de bajo nivel que se utiliza se llama, mira tú por donde, box. Es decir, antes de hacer la llamada a GetType() se está convirtiendo el número en un tipo por referencia, y luego en la siguiente línea ya se llama a la función que buscábamos en la clase base Object. Esto es una operación de boxing igual a la que vimos en el caso anterior.
Sin embargo en la primera llamada a ToString() se realiza la llamada de la manera normal y no ocurre boxing alguno. ¿Por qué?
Pues porque da la casualidad de que la estructura System.Int32 que es la que alberga a los números enteros, tiene redefinido el método ToString() por lo que se puede llamar directamente desde el tipo por valor. Sin embargo no redefine GetType() por lo que para poder usarlo se debe convertir primero en un objeto mediante la operación de boxing y luego llamar al método.
Como vemos, dos líneas aparentemente iguales por debajo actúan de una manera muy diferente.
Sutilezas con las que podemos evitar el boxing
Un caso muy común de boxing debido a esta causa es cuando utilizamos el método String.Format() para crear una cadena a partir varios datos, o lo que es casi lo mismo, el método Console.WriteLine().
Por ejemplo, si escribimos algo como esto:
Console.WriteLine("El valor de mi variable es: {1}", num);
Estaremos provocando un boxing, ya que WriteLine espera como parámetros después de la cadena de formato, objetos genéricos. Así que para llamar al método primero se convierte num en un objeto y luego se le pasa.
Sin embargo si escribimos esto otro, muy parecido:
Console.WriteLine("El valor de mi variable es: {1}", num.ToString());
En este caso no existirá boxing, ya que ahora no le estamos pasando un tipo por valor, sino un tipo por referencia (una cadena). El resultado es exactamente el mismo, pero gracias a esto hemos evitado hacer el boxing y hemos mejorado el rendimiento (imperceptiblemente en este caso, pero…). Además como hemos visto hace un momento el ToString() no provoca tampoco boxing por estar definido en Int32.
¿Qué importancia tiene todo esto?
Las operaciones de boxing y unboxing tienen un impacto en el rendimiento y en la gestión de la memoria. Como ya sabemos, el manejo de valores en la pila es más rápido y eficiente que en el montón, y además la propia operación de envolver el tipo por valor para meterlo en el montón y luego referenciarla desde la pila es costosa también.
La importancia de este efecto en aplicaciones normales es imperceptible. Sin embargo, en aplicaciones donde el rendimiento es crucial y se realizan potencialmente muchos millones de operaciones de boxing y/o unboxing, tener el concepto claro y evitarlo es un algo muy importante y puede impactar mucho en el rendimiento.
En cualquier caso, es importante conocer el concepto y saber identificarlo cuando sea necesario.
¡Espero que hayas aprendido algo interesante!