Ir al contenido principal

Genéricos en C#

Los genéricos representan una de las características más poderosas y elegantes de C#, permitiendo escribir código reutilizable, eficiente y con seguridad de tipos. Introducidos en C# 2.0, los genéricos nos permiten crear clases, interfaces, métodos y delegados que pueden trabajar con cualquier tipo de datos, manteniendo la verificación de tipos en tiempo de compilación. Esta característica elimina la necesidad de boxing/unboxing, reduce la duplicación de código y proporciona un rendimiento superior comparado con las alternativas no genéricas.

Dominar los genéricos es esencial para cualquier desarrollador de C#, ya que son fundamentales en el framework .NET y en el desarrollo de bibliotecas robustas. Desde las colecciones básicas como List<T> hasta patrones avanzados de diseño, los genéricos están presentes en prácticamente todos los aspectos del desarrollo moderno en C#.

Conceptos fundamentales de los genéricos

¿Qué son los genéricos?

Los genéricos permiten definir clases, interfaces, métodos y delegados con parámetros de tipo que se especifican en el momento de uso. Esto proporciona reutilización de código manteniendo la seguridad de tipos.

Aspecto Sin Genéricos Con Genéricos
Seguridad de tipos Verificación en tiempo de ejecución Verificación en tiempo de compilación
Rendimiento Boxing/unboxing para tipos valor No hay boxing/unboxing
Reutilización Duplicación de código o uso de Object Una implementación para todos los tipos
Legibilidad Casting explícito requerido Tipo explícito sin casting

Ventajas de los genéricos

Ventaja Descripción Ejemplo
Seguridad de tipos Errores detectados en compilación List<int> no acepta strings
Rendimiento Eliminación de boxing para tipos valor List<int> vs ArrayList
Reutilización Una implementación para múltiples tipos List<T> funciona con cualquier tipo
Legibilidad Código más claro y expresivo Dictionary<string, int> vs Hashtable

Métodos genéricos

Los métodos genéricos nos permiten crear funciones que pueden trabajar con diferentes tipos:

using System;

class UtilidadesGenericas
{
    // Método genérico simple para intercambiar valores
    public static void Intercambiar<T>(ref T valor1, ref T valor2)
    {
        T temporal = valor1;
        valor1 = valor2;
        valor2 = temporal;
    }
    
    // Método genérico para encontrar el máximo de dos valores
    public static T Maximo<T>(T valor1, T valor2) where T : IComparable<T>
    {
        return valor1.CompareTo(valor2) > 0 ? valor1 : valor2;
    }
    
    // Método genérico para mostrar información de un array
    public static void MostrarArray<T>(T[] array, string nombre)
    {
        Console.WriteLine($"\nArray '{nombre}' de tipo {typeof(T).Name}:");
        for (int i = 0; i < array.Length; i++)
        {
            Console.WriteLine($"  [{i}] = {array[i]}");
        }
    }
    
    // Método genérico con múltiples parámetros de tipo
    public static TResultado Convertir<TEntrada, TResultado>(TEntrada entrada, Func<TEntrada, TResultado> conversor)
    {
        Console.WriteLine($"Convirtiendo {typeof(TEntrada).Name} a {typeof(TResultado).Name}");
        return conversor(entrada);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Métodos genéricos básicos ===");
        
        // Intercambiar enteros
        int a = 10, b = 20;
        Console.WriteLine($"Antes: a={a}, b={b}");
        UtilidadesGenericas.Intercambiar(ref a, ref b);
        Console.WriteLine($"Después: a={a}, b={b}");
        
        // Intercambiar strings
        string texto1 = "Hola", texto2 = "Mundo";
        Console.WriteLine($"\nAntes: texto1='{texto1}', texto2='{texto2}'");
        UtilidadesGenericas.Intercambiar(ref texto1, ref texto2);
        Console.WriteLine($"Después: texto1='{texto1}', texto2='{texto2}'");
        
        Console.WriteLine("\n=== Método genérico con restricciones ===");
        
        // Encontrar máximo con diferentes tipos
        int maxEnteros = UtilidadesGenericas.Maximo(15, 25);
        double maxDecimales = UtilidadesGenericas.Maximo(3.14, 2.71);
        string maxTextos = UtilidadesGenericas.Maximo("Banana", "Manzana");
        
        Console.WriteLine($"Máximo enteros: {maxEnteros}");
        Console.WriteLine($"Máximo decimales: {maxDecimales}");
        Console.WriteLine($"Máximo textos: '{maxTextos}'");
        
        Console.WriteLine("\n=== Mostrando arrays de diferentes tipos ===");
        
        int[] numeros = { 1, 2, 3, 4, 5 };
        string[] frutas = { "Manzana", "Banana", "Naranja" };
        bool[] estados = { true, false, true };
        
        UtilidadesGenericas.MostrarArray(numeros, "Números");
        UtilidadesGenericas.MostrarArray(frutas, "Frutas");
        UtilidadesGenericas.MostrarArray(estados, "Estados");
        
        Console.WriteLine("\n=== Método con múltiples parámetros de tipo ===");
        
        // Convertir int a string
        string numeroComoTexto = UtilidadesGenericas.Convertir(42, x => $"El número es: {x}");
        Console.WriteLine(numeroComoTexto);
        
        // Convertir string a int
        int textoComoNumero = UtilidadesGenericas.Convertir("123", int.Parse);
        Console.WriteLine($"Texto convertido a número: {textoComoNumero}");
    }
}

Clases genéricas

Las clases genéricas nos permiten crear estructuras de datos reutilizables:

using System;
using System.Collections.Generic;

// Clase genérica simple para almacenar un par de valores
public class Par<T1, T2>
{
    public T1 Primero { get; set; }
    public T2 Segundo { get; set; }
    
    public Par(T1 primero, T2 segundo)
    {
        Primero = primero;
        Segundo = segundo;
    }
    
    public override string ToString()
    {
        return $"({Primero}, {Segundo})";
    }
    
    // Método genérico dentro de una clase genérica
    public TResultado Combinar<TResultado>(Func<T1, T2, TResultado> combinador)
    {
        return combinador(Primero, Segundo);
    }
}

// Clase genérica más compleja: una pila (stack) personalizada
public class PilaPersonalizada<T>
{
    private List<T> elementos;
    
    public int Cantidad => elementos.Count;
    public bool EstaVacia => elementos.Count == 0;
    
    public PilaPersonalizada()
    {
        elementos = new List<T>();
    }
    
    public void Apilar(T elemento)
    {
        elementos.Add(elemento);
        Console.WriteLine($"Apilado: {elemento} (Total: {Cantidad})");
    }
    
    public T Desapilar()
    {
        if (EstaVacia)
            throw new InvalidOperationException("La pila está vacía");
            
        T elemento = elementos[elementos.Count - 1];
        elementos.RemoveAt(elementos.Count - 1);
        Console.WriteLine($"Desapilado: {elemento} (Restante: {Cantidad})");
        return elemento;
    }
    
    public T Tope()
    {
        if (EstaVacia)
            throw new InvalidOperationException("La pila está vacía");
            
        return elementos[elementos.Count - 1];
    }
    
    public void MostrarContenido()
    {
        Console.WriteLine($"\nContenido de la pila (tipo {typeof(T).Name}):");
        if (EstaVacia)
        {
            Console.WriteLine("  (vacía)");
            return;
        }
        
        for (int i = elementos.Count - 1; i >= 0; i--)
        {
            string indicador = i == elementos.Count - 1 ? " <- tope" : "";
            Console.WriteLine($"  [{i}] {elementos[i]}{indicador}");
        }
    }
}

// Clase genérica con restricciones múltiples
public class RepositorioEnMemoria<T> where T : class, new()
{
    private List<T> elementos;
    private int siguienteId;
    
    public RepositorioEnMemoria()
    {
        elementos = new List<T>();
        siguienteId = 1;
    }
    
    public T Crear()
    {
        T nuevoElemento = new T(); // Funciona gracias a la restricción 'new()'
        elementos.Add(nuevoElemento);
        Console.WriteLine($"Creado nuevo elemento de tipo {typeof(T).Name}");
        return nuevoElemento;
    }
    
    public List<T> ObtenerTodos()
    {
        return new List<T>(elementos); // Devolver copia
    }
    
    public int ContarElementos()
    {
        return elementos.Count;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Clase Par genérica ===");
        
        // Crear pares de diferentes tipos
        var parNumeros = new Par<int, double>(42, 3.14);
        var parTextos = new Par<string, string>("Nombre", "Juan");
        var parMixto = new Par<string, int>("Edad", 25);
        
        Console.WriteLine($"Par de números: {parNumeros}");
        Console.WriteLine($"Par de textos: {parTextos}");
        Console.WriteLine($"Par mixto: {parMixto}");
        
        // Usar método genérico dentro de la clase genérica
        string concatenacion = parTextos.Combinar((a, b) => $"{a}: {b}");
        double producto = parNumeros.Combinar((a, b) => a * b);
        
        Console.WriteLine($"Concatenación: {concatenacion}");
        Console.WriteLine($"Producto: {producto}");
        
        Console.WriteLine("\n=== Pila personalizada ===");
        
        // Pila de enteros
        var pilaEnteros = new PilaPersonalizada<int>();
        pilaEnteros.Apilar(10);
        pilaEnteros.Apilar(20);
        pilaEnteros.Apilar(30);
        pilaEnteros.MostrarContenido();
        
        Console.WriteLine($"\nTope actual: {pilaEnteros.Tope()}");
        pilaEnteros.Desapilar();
        pilaEnteros.MostrarContenido();
        
        // Pila de strings
        var pilaTextos = new PilaPersonalizada<string>();
        pilaTextos.Apilar("Primera");
        pilaTextos.Apilar("Segunda");
        pilaTextos.Apilar("Tercera");
        pilaTextos.MostrarContenido();
        
        Console.WriteLine("\n=== Repositorio en memoria ===");
        
        // El repositorio requiere clases con constructor sin parámetros
        var repositorioListas = new RepositorioEnMemoria<List<int>>();
        
        var lista1 = repositorioListas.Crear();
        lista1.Add(1);
        lista1.Add(2);
        
        var lista2 = repositorioListas.Crear();
        lista2.Add(10);
        lista2.Add(20);
        
        Console.WriteLine($"Elementos en repositorio: {repositorioListas.ContarElementos()}");
        
        var todasLasListas = repositorioListas.ObtenerTodos();
        for (int i = 0; i < todasLasListas.Count; i++)
        {
            Console.WriteLine($"Lista {i + 1}: [{string.Join(", ", todasLasListas[i])}]");
        }
    }
}

Restricciones de tipos (constraints)

Las restricciones nos permiten limitar qué tipos pueden usarse como parámetros genéricos:

using System;
using System.Collections.Generic;

// Interfaz para demostrar restricciones
public interface IProcesable
{
    void Procesar();
    string ObtenerInformacion();
}

// Clase base para demostrar restricciones
public abstract class ElementoBase
{
    public string Nombre { get; set; }
    
    protected ElementoBase(string nombre)
    {
        Nombre = nombre;
    }
    
    public virtual void MostrarInfo()
    {
        Console.WriteLine($"Elemento: {Nombre}");
    }
}

// Implementaciones concretas
public class Documento : ElementoBase, IProcesable
{
    public string Contenido { get; set; }
    
    public Documento(string nombre, string contenido) : base(nombre)
    {
        Contenido = contenido;
    }
    
    public void Procesar()
    {
        Console.WriteLine($"Procesando documento: {Nombre}");
    }
    
    public string ObtenerInformacion()
    {
        return $"Documento '{Nombre}' con {Contenido.Length} caracteres";
    }
}

public class Tarea : ElementoBase, IProcesable
{
    public bool EstaCompletada { get; set; }
    
    public Tarea(string nombre) : base(nombre)
    {
        EstaCompletada = false;
    }
    
    public void Procesar()
    {
        EstaCompletada = true;
        Console.WriteLine($"Tarea completada: {Nombre}");
    }
    
    public string ObtenerInformacion()
    {
        string estado = EstaCompletada ? "Completada" : "Pendiente";
        return $"Tarea '{Nombre}' - Estado: {estado}";
    }
}

// Clase con diferentes tipos de restricciones
public class ProcesadorGenerico<T> where T : ElementoBase, IProcesable, new()
{
    private List<T> elementos;
    
    public ProcesadorGenerico()
    {
        elementos = new List<T>();
    }
    
    // Funciona gracias a la restricción 'new()'
    public T CrearElementoPorDefecto()
    {
        return new T();
    }
    
    public void AgregarElemento(T elemento)
    {
        elementos.Add(elemento);
        Console.WriteLine($"Agregado: {elemento.ObtenerInformacion()}");
    }
    
    // Funciona gracias a las restricciones de interfaz y clase base
    public void ProcesarTodos()
    {
        Console.WriteLine($"\nProcesando {elementos.Count} elementos:");
        foreach (T elemento in elementos)
        {
            elemento.MostrarInfo();    // Método de ElementoBase
            elemento.Procesar();       // Método de IProcesable
        }
    }
    
    public void MostrarEstadisticas()
    {
        Console.WriteLine($"\nEstadísticas del procesador de {typeof(T).Name}:");
        Console.WriteLine($"  Total de elementos: {elementos.Count}");
        
        foreach (T elemento in elementos)
        {
            Console.WriteLine($"  - {elemento.ObtenerInformacion()}");
        }
    }
}

// Clase con restricción de tipo valor
public class CalculadoraNumerica<T> where T : struct, IComparable<T>
{
    public T Sumar(T a, T b)
    {
        // En C# no se puede usar + directamente con genéricos
        // Usamos dynamic como workaround para este ejemplo
        dynamic da = a;
        dynamic db = b;
        return (T)(da + db);
    }
    
    public T Maximo(T a, T b)
    {
        return a.CompareTo(b) > 0 ? a : b;
    }
    
    public T Minimo(T a, T b)
    {
        return a.CompareTo(b) < 0 ? a : b;
    }
    
    public void MostrarCalculos(T valor1, T valor2)
    {
        Console.WriteLine($"\nCalculos con {typeof(T).Name}:");
        Console.WriteLine($"  Valor 1: {valor1}");
        Console.WriteLine($"  Valor 2: {valor2}");
        Console.WriteLine($"  Suma: {Sumar(valor1, valor2)}");
        Console.WriteLine($"  Máximo: {Maximo(valor1, valor2)}");
        Console.WriteLine($"  Mínimo: {Minimo(valor1, valor2)}");
    }
}

// Método con múltiples restricciones
public static class UtilidadesConRestricciones
{
    // T debe ser un tipo referencia
    public static T CrearSiEsNulo<T>(T valor) where T : class
    {
        return valor ?? Activator.CreateInstance<T>();
    }
    
    // T debe tener constructor sin parámetros
    public static List<T> CrearLista<T>(int cantidad) where T : new()
    {
        var lista = new List<T>();
        for (int i = 0; i < cantidad; i++)
        {
            lista.Add(new T());
        }
        return lista;
    }
    
    // T debe implementar IComparable y ser un tipo valor
    public static bool EstanOrdenados<T>(T primero, T segundo) where T : struct, IComparable<T>
    {
        return primero.CompareTo(segundo) <= 0;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Restricciones de clase base e interfaz ===");
        
        // Solo funciona con tipos que hereden de ElementoBase e implementen IProcesable
        var procesadorDocumentos = new ProcesadorGenerico<Documento>();
        
        // Esto no funcionaría porque Documento no tiene constructor sin parámetros
        // var docDefault = procesadorDocumentos.CrearElementoPorDefecto();
        
        var doc1 = new Documento("Manual.pdf", "Contenido del manual de usuario...");
        var doc2 = new Documento("Reporte.docx", "Datos del reporte mensual...");
        
        procesadorDocumentos.AgregarElemento(doc1);
        procesadorDocumentos.AgregarElemento(doc2);
        procesadorDocumentos.ProcesarTodos();
        procesadorDocumentos.MostrarEstadisticas();
        
        Console.WriteLine("\n=== Restricciones de tipo valor ===");
        
        var calculadoraEnteros = new CalculadoraNumerica<int>();
        calculadoraEnteros.MostrarCalculos(15, 25);
        
        var calculadoraDecimales = new CalculadoraNumerica<double>();
        calculadoraDecimales.MostrarCalculos(3.14, 2.71);
        
        Console.WriteLine("\n=== Métodos con restricciones ===");
        
        // Restricción de tipo referencia
        string texto = null;
        string textoNoNulo = UtilidadesConRestricciones.CrearSiEsNulo(texto);
        Console.WriteLine($"Texto después de verificación: '{textoNoNulo ?? "null"}'");
        
        // Restricción de constructor sin parámetros
        var listaStrings = UtilidadesConRestricciones.CrearLista<string>(3);
        Console.WriteLine($"Lista creada con {listaStrings.Count} elementos");
        
        // Restricción de tipo valor e IComparable
        bool numerosOrdenados = UtilidadesConRestricciones.EstanOrdenados(5, 10);
        bool fechasOrdenadas = UtilidadesConRestricciones.EstanOrdenados(
            new DateTime(2023, 1, 1), 
            new DateTime(2023, 12, 31)
        );
        
        Console.WriteLine($"¿5 <= 10? {numerosOrdenados}");
        Console.WriteLine($"¿Fechas ordenadas? {fechasOrdenadas}");
    }
}

Interfaces genéricas

Las interfaces genéricas proporcionan contratos flexibles para diferentes tipos:

using System;
using System.Collections.Generic;
using System.Linq;

// Interfaz genérica básica
public interface IRepositorio<T>
{
    void Agregar(T elemento);
    T BuscarPorId(int id);
    List<T> ObtenerTodos();
    bool Eliminar(int id);
    int Contar();
}

// Interfaz genérica más específica
public interface ICache<TKey, TValue>
{
    void Establecer(TKey clave, TValue valor);
    TValue Obtener(TKey clave);
    bool Existe(TKey clave);
    void Limpiar();
    Dictionary<TKey, TValue> ObtenerTodos();
}

// Interfaz para conversión entre tipos
public interface IConversor<TEntrada, TSalida>
{
    TSalida Convertir(TEntrada entrada);
    bool PuedeConvertir(TEntrada entrada);
}

// Modelo de ejemplo
public class Producto
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    public string Categoria { get; set; }
    
    public Producto() { }
    
    public Producto(int id, string nombre, decimal precio, string categoria)
    {
        Id = id;
        Nombre = nombre;
        Precio = precio;
        Categoria = categoria;
    }
    
    public override string ToString()
    {
        return $"[{Id}] {Nombre} - {Precio:C} ({Categoria})";
    }
}

// Implementación de repositorio en memoria
public class RepositorioEnMemoria<T> : IRepositorio<T> where T : class
{
    private readonly List<T> elementos;
    private readonly Func<T, int> obtenerIdFunc;
    private int siguienteId = 1;
    
    public RepositorioEnMemoria(Func<T, int> obtenerIdFunc)
    {
        this.elementos = new List<T>();
        this.obtenerIdFunc = obtenerIdFunc;
    }
    
    public void Agregar(T elemento)
    {
        elementos.Add(elemento);
        Console.WriteLine($"Agregado: {elemento}");
    }
    
    public T BuscarPorId(int id)
    {
        return elementos.FirstOrDefault(e => obtenerIdFunc(e) == id);
    }
    
    public List<T> ObtenerTodos()
    {
        return new List<T>(elementos);
    }
    
    public bool Eliminar(int id)
    {
        var elemento = BuscarPorId(id);
        if (elemento != null)
        {
            elementos.Remove(elemento);
            Console.WriteLine($"Eliminado: {elemento}");
            return true;
        }
        return false;
    }
    
    public int Contar()
    {
        return elementos.Count;
    }
}

// Implementación de caché simple
public class CacheSimple<TKey, TValue> : ICache<TKey, TValue>
{
    private readonly Dictionary<TKey, TValue> cache;
    private readonly Dictionary<TKey, DateTime> tiemposAcceso;
    
    public CacheSimple()
    {
        cache = new Dictionary<TKey, TValue>();
        tiemposAcceso = new Dictionary<TKey, DateTime>();
    }
    
    public void Establecer(TKey clave, TValue valor)
    {
        cache[clave] = valor;
        tiemposAcceso[clave] = DateTime.Now;
        Console.WriteLine($"Cache: Establecido {clave} = {valor}");
    }
    
    public TValue Obtener(TKey clave)
    {
        if (cache.TryGetValue(clave, out TValue valor))
        {
            tiemposAcceso[clave] = DateTime.Now; // Actualizar tiempo de acceso
            Console.WriteLine($"Cache: Encontrado {clave} = {valor}");
            return valor;
        }
        
        Console.WriteLine($"Cache: No encontrado {clave}");
        return default(TValue);
    }
    
    public bool Existe(TKey clave)
    {
        return cache.ContainsKey(clave);
    }
    
    public void Limpiar()
    {
        int cantidad = cache.Count;
        cache.Clear();
        tiemposAcceso.Clear();
        Console.WriteLine($"Cache: Limpiado {cantidad} elementos");
    }
    
    public Dictionary<TKey, TValue> ObtenerTodos()
    {
        return new Dictionary<TKey, TValue>(cache);
    }
    
    public void MostrarEstadisticas()
    {
        Console.WriteLine($"\nEstadísticas del cache:");
        Console.WriteLine($"  Elementos: {cache.Count}");
        
        foreach (var kvp in tiemposAcceso.OrderByDescending(x => x.Value))
        {
            Console.WriteLine($"  {kvp.Key}: último acceso {kvp.Value:HH:mm:ss}");
        }
    }
}

// Convertidores específicos
public class ConversorProductoATexto : IConversor<Producto, string>
{
    public string Convertir(Producto entrada)
    {
        if (entrada == null) return "Producto nulo";
        
        return $"{entrada.Nombre} cuesta {entrada.Precio:C} en la categoría {entrada.Categoria}";
    }
    
    public bool PuedeConvertir(Producto entrada)
    {
        return entrada != null && !string.IsNullOrEmpty(entrada.Nombre);
    }
}

public class ConversorTextoANumero : IConversor<string, int>
{
    public int Convertir(string entrada)
    {
        if (int.TryParse(entrada, out int resultado))
        {
            return resultado;
        }
        throw new ArgumentException($"No se puede convertir '{entrada}' a número");
    }
    
    public bool PuedeConvertir(string entrada)
    {
        return int.TryParse(entrada, out _);
    }
}

// Clase que usa múltiples interfaces genéricas
public class GestorDeProductos
{
    private readonly IRepositorio<Producto> repositorio;
    private readonly ICache<int, Producto> cache;
    private readonly IConversor<Producto, string> conversor;
    
    public GestorDeProductos()
    {
        repositorio = new RepositorioEnMemoria<Producto>(p => p.Id);
        cache = new CacheSimple<int, Producto>();
        conversor = new ConversorProductoATexto();
    }
    
    public void AgregarProducto(Producto producto)
    {
        repositorio.Agregar(producto);
        cache.Establecer(producto.Id, producto);
    }
    
    public Producto ObtenerProducto(int id)
    {
        // Intentar obtener del cache primero
        if (cache.Existe(id))
        {
            return cache.Obtener(id);
        }
        
        // Si no está en cache, buscar en repositorio
        var producto = repositorio.BuscarPorId(id);
        if (producto != null)
        {
            cache.Establecer(id, producto);
        }
        
        return producto;
    }
    
    public string ConvertirProductoATexto(int id)
    {
        var producto = ObtenerProducto(id);
        
        if (conversor.PuedeConvertir(producto))
        {
            return conversor.Convertir(producto);
        }
        
        return "No se puede convertir el producto";
    }
    
    public void MostrarEstadisticas()
    {
        Console.WriteLine($"\n=== Estadísticas del Gestor ===");
        Console.WriteLine($"Productos en repositorio: {repositorio.Contar()}");
        
        if (cache is CacheSimple<int, Producto> cacheSimple)
        {
            cacheSimple.MostrarEstadisticas();
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Interfaces genéricas en acción ===");
        
        var gestor = new GestorDeProductos();
        
        // Agregar productos
        gestor.AgregarProducto(new Producto(1, "Laptop", 999.99m, "Tecnología"));
        gestor.AgregarProducto(new Producto(2, "Mouse", 25.50m, "Tecnología"));
        gestor.AgregarProducto(new Producto(3, "Escritorio", 199.99m, "Muebles"));
        
        Console.WriteLine("\n=== Obteniendo productos (cache y repositorio) ===");
        
        // Primera vez: desde repositorio al cache
        var producto1 = gestor.ObtenerProducto(1);
        Console.WriteLine($"Obtenido: {producto1}");
        
        // Segunda vez: desde cache
        var producto1Cache = gestor.ObtenerProducto(1);
        Console.WriteLine($"Obtenido: {producto1Cache}");
        
        // Producto que no existe
        var productoInexistente = gestor.ObtenerProducto(999);
        Console.WriteLine($"Producto inexistente: {productoInexistente?.ToString() ?? "null"}");
        
        Console.WriteLine("\n=== Conversión usando interfaz genérica ===");
        
        // Convertir productos a texto
        string descripcion1 = gestor.ConvertirProductoATexto(1);
        string descripcion2 = gestor.ConvertirProductoATexto(2);
        string descripcionInexistente = gestor.ConvertirProductoATexto(999);
        
        Console.WriteLine($"Descripción 1: {descripcion1}");
        Console.WriteLine($"Descripción 2: {descripcion2}");
        Console.WriteLine($"Descripción inexistente: {descripcionInexistente}");
        
        gestor.MostrarEstadisticas();
        
        Console.WriteLine("\n=== Otros convertidores ===");
        
        var conversorTexto = new ConversorTextoANumero();
        string[] numerosTexto = { "123", "456", "abc", "789" };
        
        foreach (string texto in numerosTexto)
        {
            if (conversorTexto.PuedeConvertir(texto))
            {
                int numero = conversorTexto.Convertir(texto);
                Console.WriteLine($"✅ '{texto}' -> {numero}");
            }
            else
            {
                Console.WriteLine($"❌ No se puede convertir '{texto}'");
            }
        }
    }
}

Delegados y eventos genéricos

Los delegados genéricos proporcionan mayor flexibilidad en el manejo de eventos y callbacks:

using System;
using System.Collections.Generic;

// Delegado genérico personalizado
public delegate TResult OperacionGenerica<T, TResult>(T entrada);
public delegate void NotificacionGenerica<T>(T datos, string mensaje);

// Clase para demostrar eventos genéricos
public class MonitorDeEventos<T>
{
    // Evento genérico usando Action<T>
    public event Action<T> ElementoAgregado;
    public event Action<T> ElementoEliminado;
    public event Action<T, string> ElementoModificado;
    
    // Evento genérico personalizado
    public event NotificacionGenerica<T> NotificacionPersonalizada;
    
    private List<T> elementos;
    
    public MonitorDeEventos()
    {
        elementos = new List<T>();
    }
    
    public void Agregar(T elemento)
    {
        elementos.Add(elemento);
        Console.WriteLine($"Monitor: Agregando {elemento}");
        
        // Disparar evento
        ElementoAgregado?.Invoke(elemento);
        NotificacionPersonalizada?.Invoke(elemento, "Elemento agregado al monitor");
    }
    
    public bool Eliminar(T elemento)
    {
        bool eliminado = elementos.Remove(elemento);
        if (eliminado)
        {
            Console.WriteLine($"Monitor: Eliminando {elemento}");
            ElementoEliminado?.Invoke(elemento);
            NotificacionPersonalizada?.Invoke(elemento, "Elemento eliminado del monitor");
        }
        return eliminado;
    }
    
    public void Modificar(T elemento, string descripcionCambio)
    {
        if (elementos.Contains(elemento))
        {
            Console.WriteLine($"Monitor: Modificando {elemento} - {descripcionCambio}");
            ElementoModificado?.Invoke(elemento, descripcionCambio);
        }
    }
    
    public int Cantidad => elementos.Count;
    public List<T> ObtenerTodos() => new List<T>(elementos);
}

// Clase que procesa diferentes tipos usando delegados genéricos
public class ProcesadorConDelegados
{
    // Método que acepta cualquier tipo de operación genérica
    public TResult ProcesarConOperacion<T, TResult>(T entrada, OperacionGenerica<T, TResult> operacion)
    {
        Console.WriteLine($"Procesando {typeof(T).Name} con operación que devuelve {typeof(TResult).Name}");
        return operacion(entrada);
    }
    
    // Método que aplica una función a una lista de elementos
    public List<TResult> ProcesarLista<T, TResult>(List<T> elementos, Func<T, TResult> transformacion)
    {
        Console.WriteLine($"Transformando lista de {typeof(T).Name} a {typeof(TResult).Name}");
        var resultados = new List<TResult>();
        
        foreach (T elemento in elementos)
        {
            TResult resultado = transformacion(elemento);
            resultados.Add(resultado);
            Console.WriteLine($"  {elemento} -> {resultado}");
        }
        
        return resultados;
    }
    
    // Método que filtra y transforma en una sola operación
    public List<TResult> FiltrarYTransformar<T, TResult>(
        List<T> elementos, 
        Predicate<T> filtro, 
        Func<T, TResult> transformacion)
    {
        Console.WriteLine($"Filtrando y transformando lista de {typeof(T).Name}");
        var resultados = new List<TResult>();
        
        foreach (T elemento in elementos)
        {
            if (filtro(elemento))
            {
                TResult resultado = transformacion(elemento);
                resultados.Add(resultado);
                Console.WriteLine($"  ✅ {elemento} -> {resultado}");
            }
            else
            {
                Console.WriteLine($"  ❌ {elemento} (filtrado)");
            }
        }
        
        return resultados;
    }
}

class Program
{
    // Métodos para suscribirse a eventos
    static void OnElementoAgregado<T>(T elemento)
    {
        Console.WriteLine($"  🔔 Evento: Se agregó {elemento}");
    }
    
    static void OnElementoEliminado<T>(T elemento)
    {
        Console.WriteLine($"  🔔 Evento: Se eliminó {elemento}");
    }
    
    static void OnElementoModificado<T>(T elemento, string descripcion)
    {
        Console.WriteLine($"  🔔 Evento: Se modificó {elemento} - {descripcion}");
    }
    
    static void OnNotificacionPersonalizada<T>(T elemento, string mensaje)
    {
        Console.WriteLine($"  📢 Notificación: {mensaje} -> {elemento}");
    }
    
    static void Main(string[] args)
    {
        Console.WriteLine("=== Eventos genéricos ===");
        
        // Monitor de strings
        var monitorTextos = new MonitorDeEventos<string>();
        
        // Suscribirse a eventos usando métodos genéricos
        monitorTextos.ElementoAgregado += OnElementoAgregado;
        monitorTextos.ElementoEliminado += OnElementoEliminado;
        monitorTextos.ElementoModificado += OnElementoModificado;
        monitorTextos.NotificacionPersonalizada += OnNotificacionPersonalizada;
        
        // Suscribirse usando lambdas
        monitorTextos.ElementoAgregado += texto => Console.WriteLine($"  💡 Lambda: Nuevo texto '{texto}' tiene {texto.Length} caracteres");
        
        // Realizar operaciones
        monitorTextos.Agregar("Hola");
        monitorTextos.Agregar("Mundo");
        monitorTextos.Modificar("Hola", "Cambio de mayúsculas");
        monitorTextos.Eliminar("Mundo");
        
        Console.WriteLine($"\nElementos restantes: {monitorTextos.Cantidad}");
        
        Console.WriteLine("\n=== Monitor de números ===");
        
        var monitorNumeros = new MonitorDeEventos<int>();
        
        // Suscribirse con lambdas específicas para números
        monitorNumeros.ElementoAgregado += numero => 
        {
            Console.WriteLine($"  🔢 Número agregado: {numero}");
            if (numero % 2 == 0)
                Console.WriteLine($"    (es par)");
            else
                Console.WriteLine($"    (es impar)");
        };
        
        monitorNumeros.ElementoEliminado += numero => 
            Console.WriteLine($"  ➖ Número {numero} eliminado");
        
        monitorNumeros.Agregar(10);
        monitorNumeros.Agregar(15);
        monitorNumeros.Agregar(20);
        monitorNumeros.Eliminar(15);
        
        Console.WriteLine("\n=== Delegados genéricos ===");
        
        var procesador = new ProcesadorConDelegados();
        
        // Operaciones con string -> int
        OperacionGenerica<string, int> obtenerLongitud = texto => texto.Length;
        OperacionGenerica<string, bool> esVacio = texto => string.IsNullOrEmpty(texto);
        
        string textoTest = "Hola Mundo";
        int longitud = procesador.ProcesarConOperacion(textoTest, obtenerLongitud);
        bool vacio = procesador.ProcesarConOperacion(textoTest, esVacio);
        
        Console.WriteLine($"Longitud de '{textoTest}': {longitud}");
        Console.WriteLine($"¿Está vacío? {vacio}");
        
        // Operaciones con int -> string
        OperacionGenerica<int, string> convertirATexto = numero => $"Número: {numero}";
        OperacionGenerica<int, double> elevarAlCuadrado = numero => Math.Pow(numero, 2);
        
        int numeroTest = 5;
        string textoConvertido = procesador.ProcesarConOperacion(numeroTest, convertirATexto);
        double cuadrado = procesador.ProcesarConOperacion(numeroTest, elevarAlCuadrado);
        
        Console.WriteLine($"Conversión: {textoConvertido}");
        Console.WriteLine($"Cuadrado de {numeroTest}: {cuadrado}");
        
        Console.WriteLine("\n=== Procesamiento de listas ===");
        
        List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // Transformar números a strings
        var numerosComoTexto = procesador.ProcesarLista(numeros, n => $"#{n:D2}");
        Console.WriteLine($"Números como texto: [{string.Join(", ", numerosComoTexto)}]");
        
        // Transformar números a booleanos (par/impar)
        var sonPares = procesador.ProcesarLista(numeros, n => n % 2 == 0);
        Console.WriteLine($"Son pares: [{string.Join(", ", sonPares)}]");
        
        Console.WriteLine("\n=== Filtrar y transformar ===");
        
        List<string> palabras = new List<string> { "Hola", "C#", "Programación", "Genéricos", "LINQ", "A" };
        
        // Filtrar palabras largas y convertir a mayúsculas
        var palabrasLargasMayusculas = procesador.FiltrarYTransformar(
            palabras,
            palabra => palabra.Length > 3,  // Filtro: más de 3 caracteres
            palabra => palabra.ToUpper()    // Transformación: a mayúsculas
        );
        
        Console.WriteLine($"Palabras largas en mayúsculas: [{string.Join(", ", palabrasLargasMayusculas)}]");
        
        // Filtrar números pares y calcular su cuadrado
        var cuadradosDePares = procesador.FiltrarYTransformar(
            numeros,
            n => n % 2 == 0,      // Filtro: números pares
            n => n * n            // Transformación: elevar al cuadrado
        );
        
        Console.WriteLine($"Cuadrados de números pares: [{string.Join(", ", cuadradosDePares)}]");
    }
}

Covarianza y contravarianza

La covarianza y contravarianza permiten mayor flexibilidad en el uso de tipos genéricos:

using System;
using System.Collections.Generic;

// Jerarquía de clases para demostrar covarianza/contravarianza
public abstract class Animal
{
    public string Nombre { get; set; }
    
    protected Animal(string nombre)
    {
        Nombre = nombre;
    }
    
    public virtual void HacerSonido()
    {
        Console.WriteLine($"{Nombre} hace un sonido");
    }
}

public class Perro : Animal
{
    public Perro(string nombre) : base(nombre) { }
    
    public override void HacerSonido()
    {
        Console.WriteLine($"{Nombre} ladra: ¡Guau!");
    }
    
    public void Jugar()
    {
        Console.WriteLine($"{Nombre} está jugando");
    }
}

public class Gato : Animal
{
    public Gato(string nombre) : base(nombre) { }
    
    public override void HacerSonido()
    {
        Console.WriteLine($"{Nombre} maúlla: ¡Miau!");
    }
    
    public void Dormir()
    {
        Console.WriteLine($"{Nombre} está durmiendo");
    }
}

// Interfaz covariante (out T)
public interface IProductor<out T>
{
    T Obtener();
    IEnumerable<T> ObtenerTodos();
}

// Interfaz contravariante (in T)
public interface IConsumidor<in T>
{
    void Procesar(T elemento);
    void ProcesarMultiples(IEnumerable<T> elementos);
}

// Interfaz invariante (sin out ni in)
public interface IAlmacen<T>
{
    void Almacenar(T elemento);
    T Recuperar();
}

// Implementaciones para demostrar covarianza
public class ProductorDePerros : IProductor<Perro>
{
    private List<Perro> perros;
    
    public ProductorDePerros()
    {
        perros = new List<Perro>
        {
            new Perro("Rex"),
            new Perro("Max"),
            new Perro("Luna")
        };
    }
    
    public Perro Obtener()
    {
        if (perros.Count > 0)
        {
            var perro = perros[0];
            perros.RemoveAt(0);
            Console.WriteLine($"Producido: {perro.Nombre}");
            return perro;
        }
        return null;
    }
    
    public IEnumerable<Perro> ObtenerTodos()
    {
        Console.WriteLine("Produciendo todos los perros");
        return new List<Perro>(perros);
    }
}

public class ProductorDeGatos : IProductor<Gato>
{
    private List<Gato> gatos;
    
    public ProductorDeGatos()
    {
        gatos = new List<Gato>
        {
            new Gato("Whiskers"),
            new Gato("Shadow"),
            new Gato("Mittens")
        };
    }
    
    public Gato Obtener()
    {
        if (gatos.Count > 0)
        {
            var gato = gatos[0];
            gatos.RemoveAt(0);
            Console.WriteLine($"Producido: {gato.Nombre}");
            return gato;
        }
        return null;
    }
    
    public IEnumerable<Gato> ObtenerTodos()
    {
        Console.WriteLine("Produciendo todos los gatos");
        return new List<Gato>(gatos);
    }
}

// Implementación para demostrar contravarianza
public class ProcesadorDeAnimales : IConsumidor<Animal>
{
    public void Procesar(Animal animal)
    {
        Console.WriteLine($"Procesando animal: {animal.Nombre}");
        animal.HacerSonido();
    }
    
    public void ProcesarMultiples(IEnumerable<Animal> animales)
    {
        Console.WriteLine("Procesando múltiples animales:");
        foreach (var animal in animales)
        {
            Console.WriteLine($"  - {animal.Nombre} ({animal.GetType().Name})");
        }
    }
}

public class CuidadorDeAnimales : IConsumidor<Animal>
{
    public void Procesar(Animal animal)
    {
        Console.WriteLine($"Cuidando a {animal.Nombre}:");
        Console.WriteLine($"  - Alimentando a {animal.Nombre}");
        Console.WriteLine($"  - Revisando salud de {animal.Nombre}");
        animal.HacerSonido();
    }
    
    public void ProcesarMultiples(IEnumerable<Animal> animales)
    {
        Console.WriteLine("Cuidando múltiples animales:");
        foreach (var animal in animales)
        {
            Console.WriteLine($"  ❤️ Cuidando a {animal.Nombre}");
        }
    }
}

// Método helper para demostrar covarianza
public static class DemostradorCovarianza
{
    // Este método acepta IProductor<Animal> debido a la covarianza
    public static void ProcesarProductor(IProductor<Animal> productor, string tipoProductor)
    {
        Console.WriteLine($"\n--- Procesando {tipoProductor} ---");
        
        // Obtener un animal
        var animal = productor.Obtener();
        if (animal != null)
        {
            Console.WriteLine($"Obtenido: {animal.Nombre} ({animal.GetType().Name})");
            animal.HacerSonido();
        }
        
        // Obtener todos los animales restantes
        var todosLosAnimales = productor.ObtenerTodos();
        Console.WriteLine($"Animales restantes: {string.Join(", ", todosLosAnimales.Select(a => a.Nombre))}");
    }
}

// Método helper para demostrar contravarianza
public static class DemostradorContravarianza
{
    // Este método puede usar IConsumidor<Animal> para procesar Perros o Gatos
    public static void UsarConsumidor<T>(IConsumidor<Animal> consumidor, List<T> elementos, string tipoConsumidor) 
        where T : Animal
    {
        Console.WriteLine($"\n--- Usando {tipoConsumidor} con {typeof(T).Name}s ---");
        
        if (elementos.Count > 0)
        {
            // Procesar primer elemento
            consumidor.Procesar(elementos[0]);
            
            // Procesar todos los elementos
            consumidor.ProcesarMultiples(elementos.Cast<Animal>());
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("=== Covarianza en acción ===");
        Console.WriteLine("(IProductor<Perro> se puede usar como IProductor<Animal>)");
        
        // Crear productores específicos
        var productorPerros = new ProductorDePerros();
        var productorGatos = new ProductorDeGatos();
        
        // Covarianza: IProductor<Perro> se puede usar como IProductor<Animal>
        IProductor<Animal> productorAnimales1 = productorPerros; // ✅ Covarianza
        IProductor<Animal> productorAnimales2 = productorGatos;  // ✅ Covarianza
        
        // Usar los productores como productores de animales
        DemostradorCovarianza.ProcesarProductor(productorAnimales1, "Productor de Perros (como Animal)");
        DemostradorCovarianza.ProcesarProductor(productorAnimales2, "Productor de Gatos (como Animal)");
        
        Console.WriteLine("\n=== Contravarianza en acción ===");
        Console.WriteLine("(IConsumidor<Animal> se puede usar como IConsumidor<Perro> o IConsumidor<Gato>)");
        
        // Crear consumidores de animales
        var procesador = new ProcesadorDeAnimales();
        var cuidador = new CuidadorDeAnimales();
        
        // Crear listas de animales específicos
        var perros = new List<Perro> 
        { 
            new Perro("Buddy"), 
            new Perro("Charlie") 
        };
        
        var gatos = new List<Gato> 
        { 
            new Gato("Fluffy"), 
            new Gato("Snowball") 
        };
        
        // Contravarianza: IConsumidor<Animal> se puede usar como IConsumidor<Perro>
        IConsumidor<Perro> consumidorPerros = procesador; // ✅ Contravarianza
        IConsumidor<Gato> consumidorGatos = cuidador;     // ✅ Contravarianza
        
        // Usar contravarianza
        DemostradorContravarianza.UsarConsumidor(procesador, perros, "Procesador");
        DemostradorContravarianza.UsarConsumidor(cuidador, gatos, "Cuidador");
        
        Console.WriteLine("\n=== Ejemplo práctico: Sistema de notificaciones ===");
        
        // Delegate covariante
        Func<Animal> fabricaAnimales1 = () => new Perro("Factory Dog");
        Func<Animal> fabricaAnimales2 = () => new Gato("Factory Cat");
        
        // Delegate contravariante
        Action<Perro> procesadorPerros = perro => Console.WriteLine($"Procesando perro específico: {perro.Nombre}");
        Action<Gato> procesadorGatos = gato => Console.WriteLine($"Procesando gato específico: {gato.Nombre}");
        
        // Usar delegates con covarianza/contravarianza
        Action<Animal> procesadorGeneral = animal => 
        {
            Console.WriteLine($"Procesador general: {animal.Nombre}");
            animal.HacerSonido();
        };
        
        // Contravarianza en delegates
        procesadorPerros = procesadorGeneral; // ✅ Contravarianza
        procesadorGatos = procesadorGeneral;  // ✅ Contravarianza
        
        var miPerro = new Perro("Rover");
        var miGato = new Gato("Whiskers");
        
        procesadorPerros(miPerro);
        procesadorGatos(miGato);
        
        Console.WriteLine("\n=== Limitaciones: Invarianza ===");
        Console.WriteLine("(IAlmacen<T> no es covariante ni contravariante)");
        
        // Esto NO funcionaría debido a la invarianza:
        // IAlmacen<Animal> almacenAnimales = new AlmacenDePerros(); // ❌ Error de compilación
        
        Console.WriteLine("IAlmacen<T> debe usar el tipo exacto porque puede tanto recibir como devolver T");
    }
}

Resumen

Los genéricos en C# representan una herramienta fundamental para escribir código eficiente, reutilizable y con seguridad de tipos. Hemos explorado desde los métodos genéricos básicos hasta conceptos avanzados como la covarianza y contravarianza, pasando por clases genéricas, interfaces genéricas, restricciones de tipos y delegados genéricos.

La capacidad de crear código que funciona con cualquier tipo mientras mantiene la verificación en tiempo de compilación elimina muchos errores comunes y mejora significativamente el rendimiento al evitar boxing/unboxing innecesarios. Las restricciones de tipos nos permiten definir requisitos específicos para los parámetros genéricos, mientras que la covarianza y contravarianza proporcionan flexibilidad adicional en el uso de tipos relacionados. Dominar estos conceptos es esencial para aprovechar al máximo el framework .NET y crear bibliotecas robustas y eficientes.