Comenzaremos hablando de los generics en el lenguaje C#, que son el mecanismo de implementación de clases parametrizadas.
Los generics son el mecanismo de implementación de clases parametrizadas introducido en la versión 2.0 del lenguaje C#. Una clase parametrizada es exactamente igual a una clase de las habituales, salvo por un pequeño detalle: su definición contiene algún elemento que depende de un parámetro que debe ser especificado en el momento de la declaración de un objeto de dicha clase.
Esto puede resultar extremadamente útil a la hora de programar clases genéricas, capaces de implementar un tipado fuerte sin necesidad de conocer a priori los tipos para los que serán utilizadas. ¿Confuso? Mejor lo vemos con un ejemplo.
Sabemos que un ArrayList es un magnífico contenedor de elementos y que, por suerte o por desgracia, según se vea, trabaja con el tipo base object. Esto hace que sea posible almacenar en él referencias a cualquier tipo de objeto descendiente de este tipo (o sea, todos), aunque esta ventaja se torna inconveniente cuando se trata de controlar los tipos de objeto permitidos. En otras palabras, nada impide lo siguiente:
ArrayList al = new ArrayList();
al.Add("Siete caballos vienen de Bonanza...");
al.Add(7);
al.Add(new String('*', 25)); // 25 asteriscos
Esto puede provocar errores a la hora de recuperar los elementos de la lista, sobre todo si asumimos que los elementos deben ser de un determinado tipo. Y claro, el problema es que el error ocurriría en tiempo de ejecución, cuando muchas veces es demasiado tarde:
foreach (string s in al)
{
System.Console.WriteLine(s);
}
Efectivamente, se lanza una excepción indicando que "No se puede convertir un objeto de tipo 'System.Int32' al tipo 'System.String'". Lógico.
Obviamente eso se puede solucionar fácilmente, por ejemplo creando nuestra propia colección partiendo de CollectionBase (o similar) y mostrar métodos de acceso a los elementos con tipado fuerte, o bien, usando delegación, crear una clase de cero que implemente interfaces como IEnumerable en cuyo interior exista una colección que es la que realmente realiza el trabajo.
En cualquier caso, es un trabajazo, puesto que por cada clase que queramos contemplar deberíamos crear una clase específica, tal y como se describe en el párrafo anterior.
Y aquí es donde los generics entran en escena. El siguiente código declara una lista de elementos de tipo AlgoComplejo:
List<AlgoComplejo> lista = new List<AlgoComplejo>();
AlgoComplejo algo = new AlgoComplejo();
lista.Add(algo);
lista.Add(new AlgoComplejo());
lista.Add("blah"); // ¡Error en compilación!
Con esta declaración, no será posible añadir a la lista objetos que no sean de la clase indicada, ni tampoco será necesario realizar un cast al obtener los elementos, pues serán directamente de ese tipo.
Es interesante ver la gran cantidad de clases genéricas para el tratamiento de colecciones que se incorporaron con la versión 2.0 del framework en el espacio de nombres System.Collections.Generic, y su amplia utilización a lo largo del marco de trabajo.
Creación de clases genéricas
En los ejemplos anteriores hemos visto cómo utilizar las clases genéricas proporcionadas por el framework, pero, ¿y si queremos nosotros crear una clase genérica propia? Veremos que es muy sencillo.Vamos a desarrollar un ejemplo completo donde podamos ver las particularidades sintácticas y detalles a tener en cuenta. Crearemos la clase CustodiadorDeObjetos, cuya misión es almacenar un objeto genérico y permitirnos recuperarlo en cualquier momento. Básicamente, construiremos una clase con una variable de instancia y un getter y setter para acceder a la misma, pero usaremos los generics para asegurar que valga para cualquier tipo de datos y que el objeto introducido sea siempre del mismo tipo que el que se extrae:
public class CustodiadorDeObjetos<T>
{
private T objeto;
public T Objeto
{
get { return objeto; }
set { this.objeto = value; }
}
}
Como podemos ver, en la propia definición de la clase indicamos que ésta será una plantilla que recibirá como parámetro genérico un tipo al que hemos llamado T en el ejemplo anterior.
Se puede apreciar cómo hemos declarado un miembro privado llamado objeto, de tipo T, al que se puede acceder a través del correspondiente getter y setter. El tipo concreto sobre el que actuaremos se definirá en el momento de la instanciación de un objeto, puesto que habrá que indicarlo expresamente:
// Crea un custodiador de strings...
CustodiadorDeObjetos<string> cs = new CustodiadorDeObjetos<string>();
// Creamos un custodiador de ints
CustodiadorDeObjetos<int> ci = new CustodiadorDeObjetos<int>();
// Creamos un custodiador de Cosas
CustodiadorDeObjetos<Cosa> cp = new CustodiadorDeObjetos<Cosa>();
De esta forma, evitamos tener que crear clases específicas para cada tipo de datos con el que necesitemos trabajar, ahorrándonos muchísimo esfuerzo y manteniendo todas las ventajas que el tipado fuerte nos ofrece.
Y para que os hagáis una idea del potencial de esta técnica, pensad que si no existiera la genericidad (como ocurría en versiones anteriores del lenguaje), nos veríamos obligados a implementar clases específicas como las siguientes:
public class CustodiadorDeStrings
{
private string objeto;
public string Objeto
{
get { return objeto; }
set { this.objeto = value; }
}
}
public class CustodiadorDeCosas
{
private Cosa objeto;
public Cosa Objeto
{
get { return objeto; }
set { this.objeto = value; }
}
}
// ... etc.
El siguiente código muestra la utilización de la nueva clase genérica CustodiadorDeObjetos
CustodiadorDeObjetos<string> cs = new CustodiadorDeObjetos<string>();
Y para verle el sentido a esto, utilicemos el siguiente ejemplo, aparentemente correcto:
public class Seleccionador<T>
Se trata de una clase genérica abierta, cuya única operación (Mayor(...)) permite obtener el objeto mayor de los dos que le pasemos como parámetros. El criterio comparativo lo pondrá la propia clase sobre la que se instancie la plantilla: los enteros serán según su valor, las cadenas según su orden alfabético, etc.
A continuación creamos dos instancias partiendo de la plantilla anterior, y concretando el generic a los tipos que nos hacen falta:
Seleccionador<int> selInt = new Seleccionador<int>();
Estas dos instanciaciones son totalmente correctas, ¿verdad? Si después de ellas usamos el siguiente código:
Console.WriteLine(selInt.Mayor(3, 5));
Obtendremos por consola un 5 y una X. Todo perfecto; aparece, para cada llamada, la conversión a cadena del objeto mayor de los dos que le hayamos enviado como parámetros.
El problema aparece cuando instanciamos la clase genérica para un tipo que no implementa IComparable, que se utiliza en el método Mayor() para determinar el objeto mayor de ambos. En este caso, se lanza una excepción en ejecución indicando que el cast hacia IComparable no puede ser realizado, abortando el proceso. Por ejemplo:
public class MiClase // No es comparable
Una posible solución sería, antes del cast a IComparable en el método Mayor(), hacer una comprobación de tipos y realizar la conversión sólo si es posible, pero, ¿y qué hacemos en caso contrario? ¿devolver un nulo? La pregunta no creo que tenga una respuesta sencilla, puesto que en cualquier caso, se estarían intentado comparar dos objetos que no pueden ser comparados.
La solución óptima, como casi siempre, es controlar en tiempo de compilación lo que podría ser una fuente de problemas en tiempo de ejecución. La especificación de C# incluye la posibilidad de definir constraints o restricciones en la declaración de la clase genérica, limitando los tipos con los que el generic podrá ser instanciado. Y, además, de forma bastante simple, nada más que añadir en la declaración de la clase Seleccionador la siguiente cláusula where:
public class Seleccionador<T>
Existen varios tipos de restricciones que podemos utilizar para limitar los tipos permitidos para nuestros tipos genéricos:
public class Seleccionador<T>
Estas informaciones se completarán en el siguiente artículo en el que hablaremos de los Métodos genéricos en C#.
cs.Objeto = "Hola"; // Asignamos directamente
string s = cs.Objeto; // No hace falta un cast,
// puesto que Objeto es de tipo string
cs.Objeto = 12; // Error en compilación,
// Objeto es de tipo string
CustodiadorDeObjetos<int> ci = new CustodiadorDeObjetos<int>();
ci.Objeto = 12; // Asignamos directamente
int i = cs.Objeto; // No hace falta un cast, pues Objeto es int
cs.Objeto = "Hola"; // Error en compilación,
// Objeto es de tipo int
Restricción de tipos en generics
Una particularidad interesante en la declaración de los generics en C# es la posibilidad de establecer limitaciones en los tipos utilizados como parámetros de las plantillas. Aunque resulte paradójico, el propio lenguaje facilita la creación de clases genéricas que no lo sean tanto, lo cual puede resultar realmente útil en múltiples escenarios.
{
public T Mayor(T x, T y)
{
int result = ((IComparable)x).CompareTo(y);
if (result > 0)
return x;
else
return y;
}
}
Seleccionador<string> selStr = new Seleccionador<string>();
Console.WriteLine(selStr.Mayor("X", "M"));
{
}
[...]
Seleccionador<MiClase> sel = new Seleccionador<MiClase>();
MiClase x1 = new MiClase();
MiClase x2 = new MiClase();
Console.WriteLine(selString.Mayor(x1, x2)); // Excepción,
// no son
// comparables!
where T: IComparable // !Sólo permitimos comparables!
{
public Tipo Mayor(Tipo x, Tipo y)
[...]
Estas restricciones pueden combinarse, de forma que si queremos que un parámetro genérico se ciña a varias de ellas, simplemente las separamos por coma en la cláusula where:
where T: IComparable, new () // Sólo permitimos comparables,
// instanciable sin parámetros
José María Aguilar
Consultor y desarrollador independiente, Most Valuable Professional (MVP) en ASP...