Los genéricos en C# son una característica que nos permite definir clases, interfaces y métodos dependientes de un tipo de dato (qué, logicamente, que puede variar).
Este tipo de dato se especifica en tiempo de compilación, proporcionando flexibilidad y seguridad de tipo sin sacrificar el rendimiento.
Los genéricos nos permiten escribir métodos y clases que funcionan con distintos tipo de dato, a la vez que mantienen la detección de errores de tipo en tiempo de compilación
Creación y uso de clases genéricas
Definición de una clase genérica
Para definir una clase genérica, se utiliza el símbolo <T>
después del nombre de la clase. Aquí, T
es un parámetro de tipo que puede ser reemplazado por cualquier tipo específico cuando se crea una instancia de la clase.
public class MiClase<T>
{
private T contenido;
public MiClase(T contenido)
{
this.contenido = contenido;
}
public T ObtenerContenido()
{
return contenido;
}
public void MostrarContenido()
{
Console.WriteLine($"El contenido de la caja es: {contenido}");
}
}
En realidad <T>
es un convencionalismo, heredado del Templating (una característica de C++). Pero cualquier otro nombre es posible. Por ejemplo, así
public class MiClase<tipo1>
{
// ... contenido de MiClase
}
En muchas ocasiones es frecuente usar nombres descriptivos para los parámetros de tipo, como mejora de la legibilidad del código. En lugar de T
, considera usar nombres como TElemento
, TResultado
, etc.
Instanciación de una clase genérica
Ahora, para instanciar una clase genérica, debemos especificar el tipo de dato que se utilizará en lugar del parámetro de tipo T
(o como hayáis llamado al tipo).
// Para integer
MiClase<int> cajaDeEnteros = new MiClase<int>(123);
cajaDeEnteros.MostrarContenido(); // El contenido de la caja es: 123
// Para string
MiClase<string> cajaDeCadenas = new MiClase<string>("Hola");
cajaDeCadenas.MostrarContenido(); // El contenido de la caja es: Hola
Interfaces y delegados genéricos
Existen otros tipos de objetos que pueden ser genéricos. Pero el funcionamiento es el mismo en todos ellos. Por ejemplo, las interfaces y los delegados también pueden ser genéricos,
Así sería un interface genérico,
public interface IRepositorio<T>
{
void Agregar(T item);
T Obtener(int id);
}
Mientras que un delegado genérico sería así,
public delegate T Funcion<T>(T arg);
Genéricos en colecciones
Las colecciones genéricas en .NET (en el espacio de nombres System.Collections.Generic
) son muy utilizadas, y ofrecen una alternativa más segura y eficiente a las colecciones no genéricas.
Algunas de las colecciones genéricas más comunes incluyen:
List<T>
Dictionary<TKey, TValue>
Queue<T>
Stack<T>
Por ejemplo, veamos como crear una List
para números enteros y para cadenas de textos.
// lista enteros
List<int> numeros = new List<int> { 1, 2, 3, 4, 5 };
// lista de cadenas de texto
List<string> numeros = new List<string> { "A", "B", "C", "D", "E" };
Restricciones de tipos genéricos
Las restricciones permiten limitar los tipos que se pueden usar como argumentos para los parámetros de tipo. Esto se logra utilizando la palabra clave where
.
public class Almacen<T> where T : class
{
private List<T> items = new List<T>();
public void Agregar(T item)
{
items.Add(item);
}
public T Obtener(int indice)
{
return items[indice];
}
}
En este ejemplo, la restricción where T : class
asegura que solo se puedan usar tipos de referencia como argumentos de tipo para T
.
Algunas restricciones comunes incluyen:
Condición | Descripción |
---|---|
T: struct | El tipo debe ser un tipo de valor |
T: class | El tipo debe ser una referencia |
T: new() | El tipo debe tener un constructor sin parámetros. |
T: <BaseClass> | El tipo debe ser o derivar de una clase base específica |
T: <Interface> | El tipo debe implementar una interfaz específica |
Métodos genéricos con múltiples tipos
Un método genérico con múltiples parámetros de tipo permite especificar más de un tipo en la definición del método.
public class Convertidor
{
public TResult Convertir<TInput, TResult>(TInput input)
{
TResult resultado = // lo que fuera con input
return resultado;
}
}
En este ejemplo, el método Convertir
acepta un parámetro de tipo TInput
y devuelve un valor de tipo TResult
.