Qué son los Generics en C# y para qué sirven
Imagina que escribes un método que debe funcionar con diferentes tipos de datos: números enteros, cadenas, objetos. Sin Generics (genéricos), tendrías que duplicar el código para cada tipo o usar object con la posterior conversión de tipos. Ambos enfoques generan problemas: el primero conduce a redundancia, el segundo a errores en tiempo de ejecución.
Los Generics (tipos genéricos) son un mecanismo en C# que permite crear clases, métodos, interfaces y delegados que funcionan con cualquier tipo de datos, manteniendo al mismo tiempo una tipificación estricta. En lugar de escribir código para un tipo específico, usas un parámetro de tipo (generalmente denotado con la letra T), que se reemplaza por el tipo real al usarlo.
Principales ventajas de los Generics:
- Seguridad de tipos — el compilador verifica la corrección del uso de tipos en tiempo de compilación
- Rendimiento — ausencia de boxing/unboxing para tipos de valor
- Reutilización de código — una clase/método funciona con múltiples tipos
- Legibilidad — el código se vuelve más limpio y comprensible
El problema sin Generics: ejemplo con object
Antes de la aparición de los Generics, los desarrolladores solían usar el tipo object para crear métodos universales. Veamos un ejemplo:
public class Stack{ private object[] _items = new object[10]; private int _count = 0; public void Push(object item) { _items[_count++] = item; } public object Pop() { return _items[--_count]; } // Uso Stack stack = new Stack(); stack.Push(123); // Boxing de int a object stack.Push("hello"); // Se puede agregar cualquier tipo int number = (int)stack.Pop(); // Unboxing y conversión}Los problemas son evidentes:
- Al extraer se necesita una conversión explícita de tipo
- Se puede agregar accidentalmente un valor de otro tipo
- Para tipos de valor (int, double) ocurre boxing/unboxing, lo que reduce el rendimiento
Fundamentos del trabajo con Generics en C#
Creación de una clase genérica
Veamos el mismo ejemplo de la pila, pero usando Generics:
public class Stack<T>{ private T[] _items = new T[10]; private int _count = 0; public void Push(T item) { _items[_count++] = item; } public T Pop() { return _items[--_count]; }}
// UsoStack<int> intStack = new Stack<int>();intStack.Push(123);intStack.Push(456);int number = intStack.Pop(); // No se necesita unboxing, el tipo es conocido
Stack<string> stringStack = new Stack<string>();stringStack.Push("Hello");// stringStack.Push(123); // ¡Error de compilación! Seguridad de tiposEl parámetro <T> se indica entre corchetes angulares. Al crear una instancia, especificas el tipo concreto, y el compilador genera una versión especializada de la clase para ese tipo.
Métodos genéricos
Los Generics no solo funcionan con clases, sino también con métodos individuales:
public class Utils{ public static T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) > 0 ? a : b; }}
// Usoint maxInt = Utils.Max(5, 10); // 10string maxStr = Utils.Max("apple", "banana"); // "banana"Restricciones (Constraints) en Generics
A veces es necesario restringir los tipos que se pueden usar con un genérico. Para ello se utilizan restricciones (constraints) con la palabra clave where:
// Restricción: T debe ser un tipo de referenciapublic class Repository<T> where T : class{