sábado, 7 de mayo de 2022

Curso avanzado de C#. Empaquetado y desempaquetado de tipos de valor (Boxing and Unboxing)

El empaquetado (boxing) es el proceso de convertir un tipo de valor como un int o booleano en un objeto o una interfaz que sea compatible con el tipo de valor. El desempaquetado (unboxing) es el proceso de convertir un valor empaquetado en su valor original. Por ejemplo, el siguiente código crea una variable entera y luego crea un objeto que hace referencia a su valor: 

// Declaramos  e inicializamos el entero i. 

int i = 10; 

// Empaquetamos i

object iObject = i; 

Después de ejecutar este código, la variable iObject es un objeto que hace referencia al valor 10.

Curso avanzado de C#. Empaquetado y desempaquetado de tipos de valor (Boxing and Unboxing)


El Empaquetamiento (boxing) y el desempaqutamiento (unboxing) consumen mucho más tiempo que simplemente asignar un tipo de variable de valor igual a otro, por lo que debemos evitar empaquetar y desempaquetar siempre que sea posible. 

Por lo general, el empaquetamiento y el desempaquetamiento se realizan  automáticamente sin tomar ninguna acción especial. Esto sucede cuando invocamos un método que espera un objeto como parámetro pero le pasamos un valor. Por ejemplo : 

int i = 2465; 

Console.WriteLine (string.Format ("i es: {0}", i)); 

La versión del método Format de la clase string que se utiliza aquí toma como parámetro una cadena con formato y una secuencia de objetos que vamos a imprimir. El método examina los objetos y los imprime adecuadamente. 

El código pasa la variable de valor i al método Format. Ese método espera un objeto como parámetro, por lo que el programa automáticamente empaqueta el valor.

Idealmente, podríamos evitar esto haciendo que el método Format tome un int como parámetro en lugar de un objeto, pero entonces, ¿qué haríamos si queremos pasar al método un objeto double, DateTime o Person? Incluso si creamos versiones sobrecargadas del método Format para manejar todos los tipos de datos básicos (int, double, string, DateTime, bool, etc.), no podríamos manejar todas las combinaciones posibles que pueden ocurrir en una lista de parámetros. 

La solución es hacer que Format tome objetos no específicos como parámetros y luego utilice la reflexión para saber cómo imprimirlos.

De forma similar, podemos utilizar objetos no específicos para los parámetros de los métodos que escribamos y luego usar la reflexión para averiguar qué hacer con los objetos. Por lo general, obtendremos mejor rendimiento si podemos utilizar un tipo de datos, una interfaz o un tipo genérico más específico para los parámetros. Incluso si estamos dispuestos a vivir con el éxito de la actuación del empaquetamiento y el desempaquetamiento tienen un efecto secundario sutil que puede llevar a un código confuso. 

Considerar nuevamente el siguiente código: 

// Declara e inicializa el entero i. 

int i = 10;

 // empaqueta i.

objeto iObject = i; 

Después de ejecuta este código, la variable iObject es un objeto que hace referencia al valor 10, pero no es el mismo valor 10 almacenado en la variable i. Eso significa que si el código cambia uno de los valores, el otro no cambia también. Por ejemplo, el siguiente código, agrega algunas declaraciones a la versión anterior: 

// Declara e inicializa el entero i. 

int i = 10;

 // empaqueta i.

objeto iObject = i; 

// Cambia los valores. 

i = 1;

iObject = 2;

// Muestra los valores. 

Console.WriteLine (i); 

Console.WriteLine (iObject); 

Este código crea una variable entera i y la empaqueta en la variable iObject. Luego establece i igual a 1 e iObject igual a 2. La salida que se obtiene es:

1

2

La variable iObject parece referirse a la variable i pero en realidad son dos valores separados. Por cierto, el método Console.WriteLine tiene muchas versiones sobrecargadas, incluida una que toma un int como parámetro, por lo que la primera declaración WriteLine en el código anterior no requiere boxing o unboxing. La segunda declaración WriteLine debe desempaquetar iObject para obtener su valor actual 2. 

La moraleja de la historia es que, si es posible, debemos evitar el empaquetado y  desempaquetado al no almacenar referencias a tipos de valores en los objetos. Si el programa empaqueta y desempaqueta automáticamente un valor como lo hace el método string.Format, generalmente no hay mucho que pueda hacer al respecto. Finalmente, no debemos declarar parámetros de método u otras variables para tener un objeto de tipo no específicado a menos que no tengamos otra opción. 

Garantizar la interoperabilidad de código no administrado 

La interoperabilidad, permite que un programa de C# utilice clases proporcionadas por código no administrado que no se haya escrito bajo el control de Common Language Runtime (CLR), que es el entorno de ejecución que ejecuta los programas C#. Los componentes ActiveX y la API de Win32 son ejemplos de código no administrado que se puede invocar desde un programa de C#. 

Las dos técnicas más comunes para permitir que los programas administrados utilicen código no administrado son COM Interop y Platform invoke (P/invoke). COM Interop se analiza brevemente en la siguiente sección. Esta sección trata sobre P/invoke. 

Para utilizar P/invoke y acceder a un recurso no administrado, como una llamada a la API, un programa primero incluye un atributo DllImport para definir los métodos no administrados que utilizará el programa administrado. El atributo DllImport es parte del espacio de nombres System.Runtime.InteropServices, por lo que muchos programas agregan ese espacio de nombres en una declaración de uso para facilitar el uso del atributo. El atributo DllImport toma parámetros que informan al programa administrado sobre un método no administrado. Los parámetros indican cosas como la DLL que contiene el método, el juego de caracteres utilizado por el método (Unicode o ANSI) y el punto de entrada en la DLL utilizada por el método. (Si omitimos esto, el valor predeterminado es el nombre del método).

El programa aplica la declaración de método con el el atributo Static extern. La declaración incluye los parámetros que requiere el método y define el tipo de retorno. Esta declaración debe estar dentro de una clase, por ejemplo en la clase que contiene el código que utiliza el método. 

Por ejemplo, el siguiente código muestra dónde se coloca la instrucción using y el atributo DllImport en el programa de ejemplo ShortPathNames (que se describe brevemente con mayor detalle). Se resalta la instrucción DllImport

Using System;

Using System.Collections.Generic;

 ... Otras declaraciones estandard "using"...

Using System.Runtime.InteropServices;

 

Namespace ShortPathNames

{

      Partial Public Class Form1 :  Form

      {

            Public Form1()

            {

                  InitializeComponent();

            }

            [DllImport("kernel32.dll", CharSet = CharSet.Auto,

            SetLastError = true)]

            Static extern uint GetShortPathName(String lpszLongPath,

            Char[] lpszShortPath, int cchBuffer);

            ... Continuamos con nuestro código ...

      }

La instrucción DllImport indica que el método está en la biblioteca kernel32.dll, que el programa debe determinar automáticamente si quiere utilizar el juego de caracteres Unicode o ANSI y que el método debe llamar a SetLastError si hay un problema. Si hay un error, el programa puede usar GetLastWin32Error para ver qué salió mal. 

La declaración del método indica que el programa utilizará el método GetShortPathName, que convierte una ruta completa a un archivo en una ruta corta que Windows puede reconocer. (Si el método usa el juego de caracteres Unicode, el nombre del método generalmente termina con una "W" para "caracteres anchos" como en GetShortPathNameW). Este método devuelve un uint y toma como parámetros una cadena, una matriz de caracteres e int. 

Nota 

A menudo, los prefijos en los nombres de los parámetros nos dan pistas sobre los propósitos de esos parámetros. En este ejemplo, lpsz significa "puntero largo a una cadena que termina en cero" y cch significa "recuento de caracteres". Si leemos la ayuda en línea para la función API GetShortPathName, encontraremos que esos prefijos tienen sentido.

El primer parámetro es la ruta del archivo que deseamos convertir en una ruta corta. Cuando llamamos al método, P/Invoke lo convierte automáticamente en una cadena terminada en nulo. El segundo parámetro debe ser un búfer preasignado donde GetShortPathName pueda almacenar sus resultados. El tercer parámetro proporciona la longitud del búfer que asignamos, por lo que GetShortPathName sabe cuánto espacio tenemos para trabajar. 

El método devuelve un uint que indica la longitud de la cadena que el método depositó en el búfer lpszLongPath

Podemos ver la sintaxis de esta instrucción DllImport mirando la firma del método aquí

En http://www.pinvoke.net podemos ver declaraciones de DllImport para una gran cantidad de funciones de la API de Win32. Cuando necesitemos utilizar una función de la API de Win32, este es un buen sitio para comenzar. 

Una vez declarado el método, el programa puede comenzar a utilizarlo. A continuación, un ejemplo de utilización del método ShortPathNames.

// Obtiene el nombre de archivo largo.

String NombreLargo = txtArchivo.Text;

// Asigna un búfer para contener el resultado.

Char[] buffer = New Char[1024];

      Long length = GetShortPathName(

      NombreLargo, Buffer,

Buffer.Length);

// Obtiene el nombre corto.

String NombreCorto = New String(Buffer);

txtNombreCorto.Text = NombreCorto.Substring(0, (Int())length);

Este código recibe una ruta larga de archivo introducida por el usuario en el control txtArchivo y asigna un búfer de 1024 caracteres para mantener la ruta corta. Después llama al método GetShortPathName, al que le pasa la ruta larga de archivo el búfer y la longitud del búfer. Una vez que el método regresa, el programa usa el búfer para inicializar una nueva cadena. Utiliza el método Substring y la longitud devuelta por GetShortPathName para truncar la cadena a su longitud adecuada y muestra el resultado. 

En general, el tipo de instrucción DllImport que hemos visto es lo suficiente para hacer el trabajo. Pero si necesitamos más control para convertir valores entre código administrado y no administrado, podemos agregar el atributo MarshalAs a los parámetros del método o al valor de retorno. El siguiente código muestra una nueva versión de la instrucción DllImport para el método GetShortPathName que utiliza atributos MarshalAs

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError=true)]

Static extern uint GetShortPathName(

      [MarshalAs(UnmanagedType.LPTStr)] string lpszLongPath,

      [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszShortPath,

      uint cchBuffer);


El primer atributo MarshalAs indica que el primer parámetro es un tipo de datos LPTStr en el código no administrado y debe tratarse como una cadena en el código administrado. El segundo atributo MarshalAs indica que el segundo parámetro es un tipo de datos LPTStr en el código no administrado y debe tratarse como un StringBuilder en el código administrado. Por supuesto, si utilizamos esta declaración, necesitaremos cambiar nuestro código para usar un StringBuilder para un búfer en lugar de una matriz de char.


No hay comentarios:

Publicar un comentario