FUNCIONES GENRICAS
Una funcin genrica define un conjunto general de operaciones que se aplicarn a diferentes tipos de datos. Tiene como parmetro el tipo de datos que le son pasados para que opere sobre ellos. Esto nos permite que pueda aplicarse el mismo procedimiento general sobre un amplio rango de datos.
Veamos como funciona con un ejemplo:
public static void intercambia<X>(ref X a,ref X b)
{
X temporal;
temporal = a;
a=b;
b= temporal;
}
public static void Main(string[] args)
{
int i = 10, j=20;
float x=10.1F, y=23.3F;
Console.WriteLine(Original i, j: {0} , {1} \n,i,j);
Console.WriteLine(Original x, y: {0} , {1} \n,x,y);
intercambia(ref i,ref j);
intercambia(ref x,ref y);
Console.WriteLine(Nuevo i, j: {0} , {1} \n,i,j);
Console.WriteLine(Nuevo x, y: {0} , {1} \n,x,y);
string fin = Console.ReadLine();
}
La lnea void intercambia<X>(ref X a,ref X b) le indica al compilador que se est comenzando una definicin genrica. X es el tipo de datos de los valores que sern intercambiados. En main se llama a la funcin intercambia() utilizando dos tipos diferentes de datos: enteros y flotantes. Puesto que es una funcin genrica, el compilador crea automticamente dos versiones de ella una que intercambia valores enteros y otra con valores flotantes.
Podemos definir ms de un tipo genrico siempre que los separemos por comas. Veamos otro ejemplo:
public static void mifunc<tipo1,tipo2>(tipo1 x, tipo2 y)
{
Console.WriteLine( {0} , {1},x,y);
}
public static void Main(string[] args)
{
mifunc(10,hola);
mifunc(0.23,10L);
string fin = Console.ReadLine();
}
Las funciones genricas son equivalentes a las sobrecargadas excepto que son ms restrictivas. Cuando las funciones se sobrecargan, dentro de ellas pueden ocurrir acciones diferentes, pero una genrica debe realizar la misma funcin general para todas las versiones.
Aunque una funcin plantilla puede sobrecargarse a s misma, si es necesario tambin es posible sobrecargarla de forma explcita. En este caso, la funcin sobrecargada redefine (u oculta) la funcin genrica relativa a esa versin especfica. Veamos una versin diferente del primer ejemplo:
public static void intercambia<X>(ref X a,ref X b)
{
X temporal;
temporal = a;
a=b;
b= temporal;
}
// Redefinicin de la versin genrica de intercambia()
public static void intercambia(int a, int b)
{
Console.WriteLine(Esto est dentro de intercambia(int, int)\n);
}
public static void Main(string[] args)
{
int i = 10, j=20;
float x=10.1F, y=23.3F;
Console.WriteLine(Original i, j: {0} , {1} \n,i,j);
Console.WriteLine(Original x, y: {0} , {1} \n,x,y);
intercambia(i,j); // llamada a la versin de intercambia() sobrecargada explcitamente
intercambia(ref x,ref y); // intercambio de floats
Console.WriteLine(Nuevo i, j: {0} , {1} \n,i,j);
Console.WriteLine(Nuevo x, y: {0} , {1} \n,x,y);
string fin = Console.ReadLine();
}
Cuando llamamos a intercambia(i,j) se invoca a la versin sobrecargada explcitamente en el programa. Esta sobrecarga permite confeccionar una versin de una funcin genrica para que se adapte a una situacin especial. Sin embargo, en general, si necesitamos tener diferentes versiones para una funcin para diferentes tipos de datos, deberamos usar funciones sobrecargadas y no plantillas.
CLASES GENRICAS
Adems de las funciones genricas podemos definir una clase genrica, esto es, una clase que define todos los algoritmos usados por ella, pero el tipo de datos que, realmente, va a ser manipulado se especificar como un parmetro cuando se creen los objetos de esa clase.
Son tiles cuando una clase contiene una lgica generalizable. Por ejemplo, el mismo algoritmo que almacena una cola de enteros funciona tambin para una cola de caracteres. Lo mismo podramos decir con una lista enlazada. El compilador generar automticamente el tipo adecuado de objeto en funcin del tipo que se especifique cuando se crea el objeto.
La forma general de la declaracin de una clase genrica se muestra aqu:
public class Generic<T>
<T> es el nombre del tipo de resguardo o parmetro de tipo que se especificar cuando se instancie la clase. Si es necesario, puede definirse ms de un tipo de datos genricos usando una lista separada por comas.
Una vez que se ha creado una clase genrica se puede crear una instancia especfica de esa clase usando la siguiente forma general:
Nombredeclase <Tipo> ob;
Tipo es el nombre del tipo de datos sobre el que trabajar la clase. Los miembros de una clase genrica son, por s mismos, genricos.
La declaracin de una clase genrica es similar a la de una funcin genrica. El tipo real de datos almacenado por la lista, en la declaracin de la clase es genrico.
Una clase plantilla puede tener ms de un tipo genrico de datos (parmetro de tipo). Simplemente hay que declarar todos los tipos de datos requeridos por la clase en una lista separada por comas. Veamos el siguiente ejemplo:
class miclase<Tipo1,Tipo2>
{
Tipo1 i;
Tipo2 j;
public miclase(Tipo1 a, Tipo2 b)
{
i=a;
j=b;
}
public void Mostrar()
{
Console.WriteLine({0} {1}\n,i,j);
}
}
class MainClass
{
public static void Main(string[] args)
{
miclase<int, double> ob1= new miclase<int, double>(10,0.23);
miclase<char, string> ob2= new miclase<char, string>(X,Esto es una prueba);
ob1.Mostrar();
ob2.Mostrar();
string fin= Console.ReadLine();
return;
}
}
}
}
PARMETROS DE TIPO
Cuando una clase genrica es instanciada, lo hace con un parmetro de tipo, enlazando la implementacin de clase incompleta y sin forma con un tipo real, creando una instancia de clase genrica concreta, como se muestra en el siguiente ejemplo:
Cliente<ItemPedido> cliente = new Cliente<ItemPedido>();
Los parmetros de tipo se especifican siempre usando el nuevo operador de genricos <>.
Aunque podemos nombrar nuestros parmetros de tipo como deseemos, por convenio se suele definir la letra T como el nombre estndar para el primer parmetro de tipo. Si nuestra clase requiere parmetros adicionales, el convenio dicta que continuemos con las letras del alfabeto siguientes: U, V Si llegamos a la Z mediante parmetros de tipo, tendramos un problema de diseo tan grande que posiblemente los genricos por s solos no lo podran solucionar.
Para situaciones en las cuales el propsito del parmetro de tipo no es suficientemente explicativo por si mismo, Microsoft recomienda usar un nombre descriptivo para el parmetro de tipo predecido de la T mayscula, como se muestra en el siguiente cdigo:
public class MiGenerico<TClaseAcesoDatos>
Para especificar mltiples parmetros de tipo en una definicin de clase, simplemente debemos separarlas con comas:
public class MiGenerico<T, U, V>
Y para instanciar una clase que requiere mltiples parmetros de tipo, separaremos los tipos con comas como sigue:
MiGenerico<int, string, object> x = new MyGenerics<int, string, object>();
APLICANDO RESTRICCIONES A LOS PARMETROS DE TIPO
Cuando tenemos un parmetro de tipo especificado en nuestra definicin de clase, en realidad hay muy poco que podamos hacer con ello por defecto. Por ejemplo, cmo podemos crear una nueva instancia de ese objeto?. Podramos pensar que el siguiente cdigo debera estar libre de errores de compilacin:
public class Cliente<T>
{
public void AgregarItemDetalle(string nombreItem)
{
T nuevoItem = new T();
items.Add(nuevoItem);
}
}
Desgraciadamente, C# no tiene forma de conocer si el tipo de datos especificado por el parmetro T tiene un constructor por defecto, as que el compilador no permitir que usemos ese tipo en una nueva sentencia sin parmetros.
Para evitar esto, debemos usar restricciones en los parmetros de tipo. Estas restricciones especifican ciertos requerimientos que el parmetro de tipo ha de cumplir. Por ejemplo, podemos especificar que cualquier tipo pasado a nuestra clase genrica deba implementar un constructor (sin parmetros) por defecto.
Todas las restricciones en las clases genricas se indican con la palabra clave where y podemos usar mltiples restricciones en el mismo parmetro separndolas con una coma. Una muestra de cmo podemos especificar una restriccin se puede ver en el siguiente cdigo:
public class Cliente<TItemDetalle> where TItemDetalle : new()
La restriccin precedente indica que el parmetro de tipo especificado por TItemDetalle debe implementar un constructor por defecto. Las siguientes lneas muestran otras formas de especificar restricciones:
public class ListaClientes<T> where T: class, IListaItems
public class Cliente<T,U> where T: new() where U: IDatoCliente
La siguiente tabla muestra todas las restricciones que podemos aplicar a los parmetros de tipo genricos.
Restricciones de Parmetros de tipo Genricos
|
|
Restriccin
|
Descripcin
|
|
where T:struct
|
Indica que T debe ser un tipo valor (excepto Nullable)
|
|
where T:class
|
Indica que T debe ser un tipo referencia, incluyendo cualquier clase, delegado o interface.
|
|
where T:new()
|
Indica que T debe implementar un constructor por defecto. La restriccin new() debe ser especificada la ltima si se usa con varias restricciones en el mismo tipo.
|
|
where T: (clase base)
|
Indica que T debe ser o derivar de la clase base indicada.
|
|
where T: (interface)
|
Indica que T debe ser o implementar el interfaz indicado.
|
|
Un patrn de diseo muy utilizado en la OOP es el patrn factory o fbrica. En este patrn, el cdigo solicita nuevas instancias de un tipo dado desde esa fbrica de tipos ms que instanciarlos directamente. Esto da a la fbrica control completo sobre el proceso de instanciacin, adems de la posibilidad de hacer cosas como cachs de tipos preinstanciaos, ejecutar llamadas a Remoting o servicios Web para obtener datos de soporte, y mucho ms.
Para prevenir que la fbrica no llegue a tener demasiadas conversiones de tipos, el patrn ofrece llamadas para el desarrollo de una fbrica nica para cada clase. De esta forma, si tenemos una clase Cliente, siempre tendremos una clase FbricaClientes. Este modelo se usa en sistemas de ORM (Object-Relational Mapping) o en Container-Managed Persistente (CMP).
Para ver como podemos hacer un uso rpido de genricos y restricciones de parmetro de tipo, echemos un vistazo al siguiente cdigo, el cual crea una clase fbrica que puede servir tanto al tipo Cliente como al tipo ClienteEspecial:
public class FabricaClientes<TCliente, U> where TCliente : Cliente, new()
{
public TCliente ObtenerCliente()
{
return new TCliente();
}
}
El cdigo precedente restringe al tipo TCliente requiriendo que sea o derive del tipo Cliente adems de implementar un constructor por defecto. Ya que el argumento TCliente ha sido restringido con new(), el cdigo puede explcitamente crear una nueva instancia de ese tipo. A travs de la potencia de los genricos, este nuevo ejemplar no es otra cosa que el tipo exacto especificado. No se requiere entonces ninguna conversin de tipos en la clase fbrica, lo cual es un gran beneficio para los desarrolladores OOP.
INTERFACES GENRICOS
Un interface genrico funciona de una forma muy similar a como lo hace una clase genrica. El interface en s mismo acepta uno o ms parmetros de tipo de a misma forma que lo hace una clase genrica. En vez de utilizar el parmetro de tipo en la definicin de la clase, el parmetro de tipo es entonces usado en las sentencias de declaracin de miembros de las que consta el interface.
Un importante beneficio de permitir parmetros de tipo genricos en los interfaces es que las clases que implementan dichos Interfaces no necesitan ejecutar ningn boxing/unboxing innecesarios. Lo mismo podramos decir de las conversiones de tipos. Boxing/unboxing son las operaciones que permiten cambiar entre tipos valor y referencia, y son operaciones costosas. Veamos un ejemplo de interface genrico:
using System;
using System.Collections.Generic;
using System.Text;
namespace Generics1
{
public interface IManejadorDatos<TTipoFila> where TTipoFila : new()
{
void AgregarRegistro (TTipoFila registro);
void BorrarRegistro(TTipoFila registro);
void ActualizarRegistro(TTipoFila registro);
void SeleccionarRegistro(TTipoFila registro);
List<TTipoFila> SeleccionaRegistros();
}
}
Lo que hace el anterior interface es indicar que cualquier clase que implemente IManejadorDatos<TTipoFila> debe proporcionar los mtodos estndar Agregar, Borrar, Actualizar y Seleccionar tpicamente asociados con objetos de acceso a datos independientemente del tipo de datos subyacente que sea almacenado. Este es un interfaz extremadamente til que nos va a permitir una gran flexibilidad.
* Basado en traduccin propia de Visual C# 2005 Unleashed de Kevin Hoffman