FUNCIONES GENÉRICAS
Una función genérica define un conjunto general de operaciones que se aplicarán a diferentes tipos de datos. Tiene como parámetro 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
{
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 línea void intercambia<X>(ref X a,ref X b) le indica al compilador que se está comenzando una definición genérica. X es el tipo de datos de los valores que serán intercambiados. En main se llama a la función intercambia() utilizando dos tipos diferentes de datos: enteros y flotantes. Puesto que es una función genérica, el compilador crea automáticamente dos versiones de ella – una que intercambia valores enteros y otra con valores flotantes.
Podemos definir más de un tipo genérico 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
{
mifunc(10,“hola”);
mifunc(0.23,10L);
string fin = Console.ReadLine();
}
Las funciones genéricas son equivalentes a las sobrecargadas excepto que son más restrictivas. Cuando las funciones se sobrecargan, dentro de ellas pueden ocurrir acciones diferentes, pero una genérica debe realizar la misma función general para todas las versiones.
Aunque una función plantilla puede sobrecargarse a sí misma, si es necesario también es posible sobrecargarla de forma explícita. En este caso, la función sobrecargada redefine (“u oculta”) la función genérica relativa a esa versión específica. Veamos una versión diferente del primer ejemplo:
public static void intercambia<X>(ref X a,ref X b)
{
X temporal;
temporal = a;
a=b;
b= temporal;
}
// Redefinición de la versión genérica de intercambia()
public static void intercambia(int a, int b)
{
Console.WriteLine(“Esto está dentro de intercambia(int, int)\n”);
}
public static void
{
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 versión de intercambia() sobrecargada explícitamente
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 versión sobrecargada explícitamente en el programa. Esta sobrecarga permite confeccionar una versión de una función genérica para que se adapte a una situación especial. Sin embargo, en general, si necesitamos tener diferentes versiones para una función para diferentes tipos de datos, deberíamos usar funciones sobrecargadas y no plantillas.
CLASES GENÉRICAS
Son útiles cuando una clase contiene una lógica generalizable. Por ejemplo, el mismo algoritmo que almacena una cola de enteros funciona también para una cola de caracteres. Lo mismo podríamos decir con una lista enlazada. El compilador generará automáticamente el tipo adecuado de objeto en función del tipo que se especifique cuando se crea el objeto.
La forma general de la declaración de una clase genérica se muestra aquí:
public class Generic<T>
<T> es el nombre del tipo de resguardo o parámetro de tipo que se especificará cuando se instancie la clase. Si es necesario, puede definirse más de un tipo de datos genéricos usando una lista separada por comas.
Una vez que se ha creado una clase genérica se puede crear una instancia específica 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 genérica son, por sí mismos, genéricos.
La declaración de una clase genérica es similar a la de una función genérica. El tipo real de datos almacenado por la lista, en la declaración de la clase es genérico.
Una clase plantilla puede tener más de un tipo genérico de datos (parámetro 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
{
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;
}
}
}
}
PARÁMETROS DE TIPO
Cuando una clase genérica es instanciada, lo hace con un parámetro de tipo, enlazando la implementación de clase incompleta y sin forma con un tipo real, creando una instancia de clase genérica concreta, como se muestra en el siguiente ejemplo:
Cliente<ItemPedido> cliente = new Cliente<ItemPedido>();
Los parámetros de tipo se especifican siempre usando el nuevo operador de genéricos <>.
Aunque podemos nombrar nuestros parámetros de tipo como deseemos, por convenio se suele definir la letra “T” como el nombre estándar para el primer parámetro de tipo. Si nuestra clase requiere parámetros adicionales, el convenio dicta que continuemos con las letras del alfabeto siguientes: “U”, “V”… Si llegamos a la “Z” mediante parámetros de tipo, tendríamos un problema de diseño tan grande que posiblemente los genéricos por sí solos no lo podrían solucionar.
Para situaciones en las cuales el propósito del parámetro de tipo no es suficientemente explicativo por si mismo, Microsoft recomienda usar un nombre descriptivo para el parámetro de tipo predecido de
public class MiGenerico<TClaseAcesoDatos>
Para especificar múltiples parámetros de tipo en una definición de clase, simplemente debemos separarlas con comas:
public class MiGenerico<T, U, V>
Y para instanciar una clase que requiere múltiples parámetros de tipo, separaremos los tipos con comas como sigue:
MiGenerico<int, string, object> x = new MyGenerics<int, string, object>();
APLICANDO RESTRICCIONES A LOS PARÁMETROS DE TIPO
Cuando tenemos un parámetro de tipo especificado en nuestra definición de clase, en realidad hay muy poco que podamos hacer con ello por defecto. Por ejemplo, ¿cómo podemos crear una nueva instancia de ese objeto?. Podríamos pensar que el siguiente código debería estar libre de errores de compilación:
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 parámetro T tiene un constructor por defecto, así que el compilador no permitirá que usemos ese tipo en una nueva sentencia sin parámetros.
Para evitar esto, debemos usar restricciones en los parámetros de tipo. Estas restricciones especifican ciertos requerimientos que el parámetro de tipo ha de cumplir. Por ejemplo, podemos especificar que cualquier tipo pasado a nuestra clase genérica deba implementar un constructor (sin parámetros) por defecto.
Todas las restricciones en las clases genéricas se indican con la palabra clave “where” y podemos usar múltiples restricciones en el mismo parámetro separándolas con una coma. Una muestra de cómo podemos especificar una restricción se puede ver en el siguiente código:
public class Cliente<TItemDetalle> where TItemDetalle : new()
La restricción precedente indica que el parámetro de tipo especificado por TItemDetalle debe implementar un constructor por defecto. Las siguientes líneas 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 parámetros de tipo genéricos.
|
||||||||||||||
Un patrón de diseño muy utilizado en
Para prevenir que la fábrica no llegue a tener demasiadas conversiones de tipos, el patrón ofrece llamadas para el desarrollo de una fábrica única para cada clase. De esta forma, si tenemos una clase Cliente, siempre tendremos una clase FábricaClientes. Este modelo se usa en sistemas de ORM (Object-Relational Mapping) o en Container-Managed Persistente (CMP).
Para ver como podemos hacer un uso rápido de genéricos y restricciones de parámetro de tipo, echemos un vistazo al siguiente código, el cual crea una clase fábrica 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 código precedente restringe al tipo TCliente requiriendo que sea o derive del tipo Cliente además de implementar un constructor por defecto. Ya que el argumento TCliente ha sido restringido con new(), el código puede explícitamente crear una nueva instancia de ese tipo. A través de la potencia de los genéricos, este nuevo ejemplar no es otra cosa que el tipo exacto especificado. No se requiere entonces ninguna conversión de tipos en la clase fábrica, lo cual es un gran beneficio para los desarrolladores OOP.
INTERFACES GENÉRICOS
Un interface genérico funciona de una forma muy similar a como lo hace una clase genérica. El interface en sí mismo acepta uno o más parámetros de tipo de a misma forma que lo hace una clase genérica. En vez de utilizar el parámetro de tipo en la definición de la clase, el parámetro de tipo es entonces usado en las sentencias de declaración de miembros de las que consta el interface.
Un importante beneficio de permitir parámetros de tipo genéricos en los interfaces es que las clases que implementan dichos Interfaces no necesitan ejecutar ningún boxing/unboxing innecesarios. Lo mismo podríamos 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 genérico:
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 métodos estándar Agregar, Borrar, Actualizar y Seleccionar típicamente 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 traducción propia de Visual C# 2005 Unleashed de Kevin Hoffman