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 algo = new AlgoComplejo();
lista.Add(algo);
lista.Add(new AlgoComplejo());
lista.Add("blah"); // ¡Error en compilación!
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
{
private T objeto;
public T Objeto
{
get { return objeto; }
set { this.objeto = value; }
}
}
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
// Creamos un custodiador de ints
CustodiadorDeObjetos
// Creamos un custodiador de Cosas
CustodiadorDeObjetos
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 que definimos con anterioridad:
CustodiadorDeObjetos
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
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
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.
Y para verle el sentido a esto, utilicemos el siguiente ejemplo, aparentemente correcto:
public class Seleccionador
{
public T Mayor(T x, T y)
{
int result = ((IComparable)x).CompareTo(y);
if (result > 0)
return x;
else
return y;
}
}
A continuación creamos dos instancias partiendo de la plantilla anterior, y concretando el generic a los tipos que nos hacen falta:
Seleccionador
Seleccionador
Estas dos instanciaciones son totalmente correctas, ¿verdad? Si después de ellas usamos el siguiente código:
Console.WriteLine(selStr.Mayor("X", "M"));
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
{
}
[...]
Seleccionador
MiClase x1 = new MiClase();
MiClase x2 = new MiClase();
Console.WriteLine(selString.Mayor(x1, x2)); // Excepción,
// no son
// comparables!
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
where T: IComparable // !Sólo permitimos comparables!
{
public Tipo Mayor(Tipo x, Tipo y)
[...]
•where T: struct, indica que el argumento debe ser un tipo valor.
•where T: class, indica que T debe ser un tipo referencia.
•where T: new(), fuerza a que el tipo T disponga de un constructor público sin parámetros; es útil cuando desde dentro de algún método de la clase se pretende instanciar un objeto del mismo.
•where T: nombredeclase, indica que el tipo T debe heredar o ser de dicha clase.
•where T: nombredeinterfaz, T deberá implementar el interfaz indicado.
•where T1: T2, indica que el argumento T1 debe ser igual o heredar del tipo, también parámetro genérico de la clase, T2.
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:
public class Seleccionador
where T: IComparable, new () // Sólo permitimos comparables,
// instanciable sin parámetros
Fuente de informacion: http://www.desarrolloweb.com/articulos/metodos-genericos-c.html
prueba
ResponderEliminar