csharp-genericos

Qué son y cómo usar los genéricos en C#

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ónDescripción
T: structEl tipo debe ser un tipo de valor
T: classEl 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.