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.