miércoles, 16 de abril de 2008

Generics en C#, o tipos genéricos en C almohadilla.

Introducción a Generics, o tipos genéricos en .net

Bueno, éste tema puede que sea conocido por muchos, otros no se animan a preguntar y otros nunca lo llegan a entender, sin embargo es algo indispensable en el desarrollo moderno y que quien lo ha probado no puede vivir sin él.

Vamos a analizar primero un pequeño trozo de código

public void PruebaGenerics1()
{
    IList enteros = new System.Collections.ArrayList();

    enteros.Add("hola");
    enteros.Add(1);
    enteros.Add(new StringBuilder());

    foreach (object o in enteros)
    {
        Console.WriteLine(o.ToString());
    }
}

Sin hacer mucho análisis nos damos cuenta que nuestra lista de enteros se encuentra expuesta a que se le pueda meter dentro cualquier cosa como vemos, y lo más interesante es que el compilador nos lo va a permitir, definitivamente esto no es deseable, es más, siendo un poco desconfiados podemos presumir que dentro del bucle cuando hacemos referencia a "o" no podemos hacer uso de ningún método propio de la clase Int32 porque efectivamente estamos diciendo que todos los elementos de la lista son objects otro efecto no deseado, bueno queda algo más, esta lista tiene un alto problema de rendimiento porque se puede meter cualquier cosa dentro, no piensen que una vez que metemos un lindo StringBuilder en ella va a ser degradado a object, no es así, sin embargo como la lista nos permite poner cualquier cosa dentro, y de hecho no sabe qué vamos a poner, el compilador no puede hacer ninguna suposición y por lo tanto ninguna optimización, vamos a comprobar si nuestro StringBuilder sigue en la lista.

public void PruebaGenerics1_2()
{
    IList enteros = new System.Collections.ArrayList();

    enteros.Add("hola");
    enteros.Add(1);

    enteros.Add(new StringBuilder().Append("hola string builder"));

    foreach (object o in enteros)
    {
        Console.WriteLine(o.ToString());
    }

}

Bien, luego de esta triste vamos a ver qué puede hacer Generics por nosotros.

Llego Generics 

La idea de Generics es poder tipificar (lo siento Cervantes pero no encontré un término en castellano para esto) algo, digo algo porque es aplicable a objetos (como las lista) a métodos, a interfaces y a clases, vamos a ver el mismo ejemplo con Generics

public void PruebaGenerics2()
{
    IList<int> enteros = new List<int>();

    enteros.Add("hola");
    enteros.Add(1);
    enteros.Add(new StringBuilder());

    foreach (int o in enteros)
    {
        Console.WriteLine(o.ToString());
    }
}

Lo primero que notamos es la sintáxis List<T> donde T es el tipo, esta instrucción en patricular declara una lista genérica de elementos del tipo T, en éste caso una lista de enteros (int). En cuanto queremos compilar el compilador nos da un error porque definimos una lista de enteros y sin embargo estamos intentando poder dentro elementos que no son enteros, primer ventaja, comprobación de tipos en tiempo de compilación. 
Dentro del bucle ahora decimos que los elementos de la lista "enteros" son del tipo int, segunda ventaja, cuando hacemos referencia a "o" dentro del bucle podemos utlizar las propiedades y métodos propios de T, en éste caso podemos hacer

 Console.WriteLine(o.CompareTo(1));

Por ejemplo, y es perfectamente válido porque el objeto "o" es de tipo int, es más cuando hacemos enteros.Add nos pide un objecto del tipo int, lo mismo que el método Insert, etc.

Generics en los métodos

Una cosa muy interesante es poder pasar un parámetro a un método de un tipo genérico, es decir, poder decirle a un método que el parámetro que le pasamos es de un tipo que establecemos en ese mismo momento, vamos a ver el código que es más claro

public void PruebaGeneric3<T>(T objetoGerico)
{
    IList listaGenerica = new List<T>();

    listaGenerica.Add(objetoGerico);

    foreach (T item in listaGenerica)
    {
        if (item is Int32)
        {
            Console.WriteLine("soy un entero que vale {0}", item);
        }
        else
        {
            Console.WriteLine("No soy entero {0}", item);
        }
    }
}

Ok, no es un ejemplo nada real, pero sirve para la explicación, primero notamos que en la declaración del método podemos luego del nombre <T>, con esto decimos que el método recibe un parámetro de tipo, y luego dentro de los parámetros que recibe decimos que recibe un elemento llamado objetoGenerico del tipo T (que no tenemos idea de lo que puede ser) es muy probable que ahora no nos demos cuenta de la utilidad de esto, pero les aseguro que la tiene y mucha. Para terminar dentro de método hacemos alegremente referencia a T sin saber de qué estamos hablando, mágico.
La forma de invocarla sería:

        claseGenerica.PruebaGeneric<int>(1);       


Generics en las clases
 
Otra utilidad de Generics de poder a nivel clase recibir un tipo y hacer referencia a él dentro de toda la clase, por ejemplo
public class PruebaGenerics6<T>
{
    public void PruebaGeneric(T objetoGerico)
    {
        IList listaGenerica = new List<T>();

        listaGenerica.Add(objetoGerico);

        foreach (T item in listaGenerica)
        {
            if (item is Int32)
            {
                Console.WriteLine("soy un entero que vale {0}", item);
            }
            else
            {
                Console.WriteLine("No soy entero {0}", item);
            }
        }
    }
}
}
Acá vemos el mismo método de antes pero en lugar de recibir como parámetro a T (que podría llamar Ta, Tb, lo que sea) en el método lo hacemos en la clase, para usarla sería
PruebaGenerics6<int> claseGenerica = new PruebaGenerics6<int>();
claseGenerica.PruebaGeneric(1);

Restricciones en Generics o Constrains en Generics

Para finalizar esta humilde introducción a Generics en C# vamos a ver una característica muy interesante que viene a solucionarnos un problema que vamos a analizar, imaginemos que tenemos la clase anterior y dentro del método PruebaGeneric necesitamos llamar a un método MetodoFantasma del tipo T, el problema es que el tipo T no lo conocemos y en caso de querer hacer la invocación al objeto "objetoGenerico.MetodoFantasma" por supuesto que el compilador nos va a dar un hermoso error diciendo (con toda razón) que le tipo T no tiene una definición para MetodoFantasma, bueno, cómo solucionamos esto, con restricciones, del siguiente modo

interface IFantasma
{
    void MetodoFantasma();
}

public class PruebaGenerics6<T> where T: IFantasma
{
    public void PruebaGeneric(T objetoGerico)
    {
        IList listaGenerica = new List<T>();

        listaGenerica.Add(objetoGerico);

        foreach (T item in listaGenerica)
        {
            item.MetodoFantasma();
        }
    }
}

Vamos a ver cómo funciona esto, primero creamos una interfece (o una clase base abstracta o no) que tenga el método que necesitamos que T tenga, y luego agregamos la restricción, en este caso "where T: IFantasma" dice que el tipo T tiene que implementar IFantasma con lo cual van a pasar dos cosas, primero vamos a poder hacer referencia al método MetodoFantasma sin necesidad de conocer el tipo porque estamos diciendo que tiene que implementar la interface, y segundo no vamos a poder pasar ningún tipo en T que no implemente dicha interface.

Conclusión

Los tipos genéricos nos permiten hacer cosas que de otro modo serían imposibles, además no permiten tener verificaciones de tipos en tiempo de compilación y como yapa quedan lindo. Para aprender Generics les recomiendo que lean éste link en ingles del mismísimo Anders Hejlsberg

Generics in C#, Java, and C++


Saludos.

PD: Dedicado a Sony Ordoñez.
 

3 comentarios:

Fragmentadora de Papel dijo...
Este comentario ha sido eliminado por un administrador del blog.
Eduardo León dijo...

Hola, sólo quería agradecerte por estos artículos que cuelgas en tu blog. Son de mucha utilidad y de mucho provecho. Desde España te doy las das gracias.
Un saludo!

Leonardo Micheloni dijo...

Eduardo,
Como digo siempre, de nada, es un pqueño aporte para la comunidad de habla hispana, ya que hay mucho en inglés y por desgracia (y no me termino de convencer por qué) muchos referentes de mi país y otros de habla hispana prefieren tener sus blogs en inglés, espero que te sea útil y un saludos desde el otro lado del charco.