Mostrando entradas con la etiqueta C#. Mostrar todas las entradas
Mostrando entradas con la etiqueta C#. Mostrar todas las entradas

viernes, 19 de enero de 2007

Eficiencia y eficacia en el código

O bien, ¿Qué es más rápido (y eficiente): Te llamo por tu nombre, o busco de qué familia eres?

Tengo un antiguo proyecto que comencé en .NET 1.1 y que tenía abandonado. Ahora que ya sé más cosas de la plataforma, quiero retomarlo y forrarme como un cerdo cuando lo venda. Pero lo voy haciendo con calma y mirando con cariño el código.

Una de las cosas que he mirado es la creación y gestión de ventanas MDI. Yo provengo del mundillo de Delphi y seguramente haga muchas cosas en .NET como las haría o hacía en Delphi (igual de bien o igual de mal xD). El caso es que para la gestión de los MDIChild, una de las formas de controlar que no creas más de una instancia de un form determinado, es recorrerte los forms abiertos comprobando a qué clase pertenecen sus instancias. En .NET se puede hacer así, o se puede hacer comprobando el nombre que se le dió a la instancia cuando se creó (lo cual se me antoja mucho más sencillo y más "lógico").

Veamos dos fragmentos de código:


... código anterior...

// Verificaremos si está creado el formulario.
bool vExist = false;
int i = 0;
// Recorremos la lista de MDIs a ver si alguno es el que buscamos.
while ((i < this.MdiChildren.Length) & (!vExist)) {
if ((Form)this.MdiChildren[i] is AfrmRejillaClientes) {
vExist = true;
((AfrmRejillaClientes)this.MdiChildren[i]).WindowState = FormWindowState.Normal;
}
i++;
}
// Si NO lo hemos encontrado...
if (!vExist) {
AfrmRejillaClientes frmRejClientes = new AfrmRejillaClientes();
frmRejClientes.MdiParent = this;
frmRejClientes.Show();
}

... código posterior...


Como se puede ver, estamos utilizando el comprobador 'is' para verificar que el tipo del formulario que estamos comprobando (nótese el cast al tipo Form sobre el objeto 'this') sea el que hemos indicado. La otra forma de hacerlo es comprobando el nombre de la instancia:


... código anterior...

// Verificaremos si está creado el formulario.
bool vExist = false;
int i = 0;
// Recorremos la lista de MDIs a ver si alguno es el que buscamos.
while ((i < this.MdiChildren.Length) & (!vExist)) {
if (((Form)this.MdiChildren[i]).Name == "AfrmRejillaClientes") {
vExist = true;
((AfrmRejillaClientes)this.MdiChildren[i]).WindowState = FormWindowState.Normal;
}
i++;
}
// Si NO lo hemos encontrado...
if (!vExist) {
AfrmRejillaClientes frmRejClientes = new AfrmRejillaClientes();
frmRejClientes.MdiParent = this;
frmRejClientes.Show();
}

... código posterior...


Una cosa curiosa es que yo pensaba que había que utilizar el método '.ToString()' y no la propiedad '.Name'. Pero bueno la cuestión es otra: ¿Cuál de las dos formas de escribir este código es más eficiente? Mi primer pensamiento fue que la más eficiente es la que comprueba el nombre. ¿Será cierto? Veamos pues...

MSIL correspondiente al primer código:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_002e
IL_0006: ldarg.0
IL_0007: call instance class [System.Windows.Forms]System.Windows.Forms.Form[] [System.Windows.Forms]System.Windows.Forms.Form::get_MdiChildren()
IL_000c: ldloc.1
IL_000d: ldelem.ref
IL_000e: isinst ProgramaGestion.Forms.AfrmRejillaClientes
IL_0013: brfalse.s IL_002a


MSIL correspondiente al segundo código:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_0038
IL_0006: ldarg.0
IL_0007: call instance class [System.Windows.Forms]System.Windows.Forms.Form[] [System.Windows.Forms]System.Windows.Forms.Form::get_MdiChildren()
IL_000c: ldloc.1
IL_000d: ldelem.ref
IL_000e: callvirt instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Name()
IL_0013: ldstr "AfrmRejillaClientes"
IL_0018: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_001d: brfalse.s IL_0034

Como se puede apreciar, evidentemente me equivocaba. La segunda opción es mucho menos eficiente que la primera. La llamada a la función 'isinst' del MSIL ocupa 4 bytes; y todo el tinglado que se monta en el segundo listado, para hacer una comparación de cadenas, es mucho más pesado (15 bytes). No me he puesto a mirar cuantos ciclos de reloj supondrían, pero seguro que gana en eficiencia y eficacia la comprobación del Tipo. De calle, además.

Que sí, que en estos tiempos de PCs con Gigabytes de memoria, Gigahertzios de frecuencia y cientos de Gigabytes de capacidad de almacenamiento, ahorrarse 15 miserables bytes puede parecer una soberana chorrada, pero mira de 15 en 15 bytes te puedes evitar que un programa te ocupe 100 Megas en memoria al ejecutarse (habéis mirado alguna vez en el administrador de tareas, el proceso 'devenv.exe'? Yo me horrorizo casi siempre). Y además es sana costumbre de buen programador el optimizar tu código hasta donde te sea posible... ¿no?

martes, 21 de noviembre de 2006

Boxing y Unboxing

Pues como ya hacía tiempo que no posteaba nada 'útil' en el blog, vamos a arrancarnos por peteneras con un didáctico articulillo sobre esta característica de .NET.

El Boxing no es otra cosa que convertir un tipo "valor" (Int, Double, Structs...) en un tipo "referencia". La utilidad de esto, se puede encontrar si en alguna ocasión necesitamos pasarle un parámetro, que por definición sería "por valor", a un método que requiere parámetros que son tipos por Referencia (esto puede ocurrir con más frecuencia de lo que pensamos, y entonces nos vemos obligados a sobrecargar nuestras funciones, o a crear nuevas -OJO no confundir con el modificador ref de C#, ByRef de VB.NET).

¿Cómo se hace el Box? Muy sencillo, tenemos la siguiente variable:


int i = 35; // Esas rimas fáciles...

Y queremos utilizarla en un método, que podría ser por ejemplo éste:

static void ManejaUnObjeto(object o)
{
...
}

Primer problema: el método recibe un objeto (un tipo por Referencia), y nosotros queremos pasarle un entero (tipo por valor)... Evidentemente no podemos incluir un ref en la llamada al método puesto que no está declarado para aceptarlo. ¿Solución? Nada mejor que "convertir" esa variable, precisamente en un objeto (la Madre De Todos los tipos por Referencia). Así conseguimos pasarle la variable al método:

object objEntero = i;
ManejaUnObjeto(objEntero);

Segundo problema: Ya tengo la variable (o algo parecido) dentro del método. ¿Cómo demonios la utilizo?

static void ManejaUnObjeto(object Objeto)
{
int j = int(Objeto);
}

Para poder utilizar el valor correctamente, debemos hacer el "Unboxing" del parámetro, que no es otra cosa que un Cast al tipo de dato correspondiente. Pero ojo, porque el tipo DEBE ser el mismo de la variable original:

static void ManejaUnObjeto(object Objeto)
{
double j = double(Objeto); // Esto pega un petardazo.
}

Tenemos dos soluciones para ésto: O bien interceptamos la excepción con un try...catch(InvalidCastException) o bien nos aseguramos de que el tipo sea el esperado (es más elegante esta opción, por supuesto):

static void ManejaUnObjeto(object Objeto)
{
if(Objeto is int)
{ int j = int(Objeto); }
}

Otro aspecto a tener en cuenta es cuando le pasamos a ese método un tipo definido por nosotros:

// Definimos esta estructura (recordar que es tipo por VALOR,
// al contrario que la Clase)

struct Estructura
{
public int i, j;
}

// Definimos dentro del Main una variable de ese tipo y la
// pasamos a un método

static void Main(string[] args)
{
Estructura struc;
struc.i = 10;
struc.j = 20;
MetodoEstructura(struc);
}

Vemos aquí que no hemos hecho el Box, es decir, no hemos convertido la estructura en objeto. Esto es perfectamente válido y no es mucho problema, en principio. Por regla general, .NET hace el Boxing automáticamente cuando se encuentra que pasamos un tipo por Valor a un método que espera un objeto ("vaya, ¿entonces para qué lo explicas?" Pues porque nunca está de más saber las cosas :P ).

Proseguimos con el código anterior. Vamos ahora a escribir el método que recibe y utiliza esta estructura:

static void MetodoEstructura(object Objeto)
{
Console.WriteLine("Valor 0 {0}, Valor 1 {1}", Objeto.i,
Objeto.j);
}

Este método ni siquiera compila. ¿Motivo? Que la clase System.Object no tiene ningún miembro llamado 'i' o 'j', y aunque el compilador es capaz de hacer el Box automático del parámetro de entrada, NO hace la acción inversa. Hemos pues de proceder al Unbox manual:

static void MetodoEstructura(object Objeto)
{
// Ahora si...
if(Objeto is Estructura)
{
Estructura s = (Estructura)Objeto;
Console.WriteLine("Valor 0 {0}, Valor 1 {1}", s.i, s.j);
}
else
Console.WriteLine("No has enviado una Estructura!");
}

Una cuestión a tener en cuenta es que las operaciones de Box y Unbox, como cualquier Cast, consumen algo de tiempo de proceso y si se utilizan con demasiada frecuencia, el rendimiento de nuestra Aplicación podría verse afectado (podría). Sin embargo desde la versión 2.0 de la plataforma hay una posible solución a esto, y es el uso de los "nuevos" tipos genéricos (parecido al Variant de toda la vida de los que conocemos Delphi, jeje :P ... Sólo que en principio el uso del tipo Variant en Delphi SI penaliza el rendimiento, y los genéricos en .NET, al parecer, no lo hacen).