Ir al contenido principal

Extensiones de métodos

Las extensiones de métodos son una característica de C# que permite añadir nuevos métodos a tipos existentes sin modificar su código fuente original ni crear un tipo derivado. Esta funcionalidad es especialmente útil cuando necesitamos ampliar la funcionalidad de tipos que no podemos modificar directamente, como los tipos del framework .NET, tipos de terceros o tipos sellados. LINQ, una de las características más populares de C#, está construida principalmente sobre extensiones de métodos.

Esta característica proporciona una forma elegante de extender funcionalidad manteniendo un código limpio y legible, permitiendo que los nuevos métodos se llamen como si fueran métodos nativos del tipo original. Su comprensión es fundamental para aprovechar al máximo las bibliotecas modernas de .NET y para crear código más expresivo y reutilizable.

Conceptos fundamentales

Definición y propósito

Una extensión de método es un método estático especial que puede ser invocado como si fuera un método de instancia del tipo que extiende. Se define en una clase estática y utiliza la palabra clave this como modificador del primer parámetro para indicar el tipo que está siendo extendido.

Sintaxis básica

La sintaxis para crear una extensión de método sigue esta estructura:

public static class ClaseExtensiones
{
    public static TipoRetorno NombreMetodo(this TipoAExtender parametro, /* otros parámetros */)
    {
        // Implementación del método
    }
}

Requisitos para las extensiones de métodos

Requisito Descripción
Clase estática La clase que contiene la extensión debe ser estática
Método estático El método de extensión debe ser estático
Modificador this El primer parámetro debe usar el modificador this
Espacio de nombres Debe estar en el mismo espacio de nombres o importado con using
Acceso público Típicamente son públicos para ser utilizables desde otros proyectos

Creación de extensiones básicas

Veamos ejemplos prácticos de cómo crear extensiones de métodos:

Extensiones para tipos básicos

using System;

// Clase estática que contiene las extensiones
public static class ExtensionesString
{
    // Extensión que verifica si una cadena es un email válido
    public static bool EsEmailValido(this string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return false;
            
        return email.Contains("@") && email.Contains(".");
    }
    
    // Extensión que capitaliza la primera letra de cada palabra
    public static string CapitalizarPalabras(this string texto)
    {
        if (string.IsNullOrWhiteSpace(texto))
            return texto;
        
        string[] palabras = texto.Split(' ');
        for (int i = 0; i < palabras.Length; i++)
        {
            if (palabras[i].Length > 0)
            {
                palabras[i] = char.ToUpper(palabras[i][0]) + palabras[i].Substring(1).ToLower();
            }
        }
        
        return string.Join(" ", palabras);
    }
    
    // Extensión que cuenta las palabras en un texto
    public static int ContarPalabras(this string texto)
    {
        if (string.IsNullOrWhiteSpace(texto))
            return 0;
            
        return texto.Split(new char[] { ' ', '\t', '\n', '\r' }, 
                          StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

// Extensiones para números enteros
public static class ExtensionesInt
{
    // Verifica si un número es par
    public static bool EsPar(this int numero)
    {
        return numero % 2 == 0;
    }
    
    // Calcula el factorial de un número
    public static long Factorial(this int numero)
    {
        if (numero < 0)
            throw new ArgumentException("El número no puede ser negativo");
            
        if (numero == 0 || numero == 1)
            return 1;
            
        long resultado = 1;
        for (int i = 2; i <= numero; i++)
        {
            resultado *= i;
        }
        
        return resultado;
    }
    
    // Repite una acción n veces
    public static void Veces(this int numero, Action accion)
    {
        for (int i = 0; i < numero; i++)
        {
            accion();
        }
    }
}

// Ejemplo de uso
class Program
{
    static void Main()
    {
        // Extensiones de string
        string email = "usuario@ejemplo.com";
        Console.WriteLine($"¿{email} es válido? {email.EsEmailValido()}");
        
        string texto = "hola mundo desde C#";
        Console.WriteLine($"Capitalizado: {texto.CapitalizarPalabras()}");
        Console.WriteLine($"Número de palabras: {texto.ContarPalabras()}");
        
        Console.WriteLine();
        
        // Extensiones de int
        int numero = 8;
        Console.WriteLine($"¿{numero} es par? {numero.EsPar()}");
        Console.WriteLine($"Factorial de 5: {5.Factorial()}");
        
        // Usar la extensión Veces
        Console.WriteLine("Repetir 3 veces:");
        3.Veces(() => Console.WriteLine("¡Hola!"));
    }
}

Extensiones para colecciones

Las extensiones son especialmente útiles para trabajar con colecciones:

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

public static class ExtensionesColeccion
{
    // Verifica si una colección está vacía o es null
    public static bool EstaVaciaONull<T>(this IEnumerable<T> coleccion)
    {
        return coleccion == null || !coleccion.Any();
    }
    
    // Obtiene un elemento aleatorio de la colección
    public static T ElementoAleatorio<T>(this IList<T> lista)
    {
        if (lista == null || lista.Count == 0)
            throw new InvalidOperationException("La lista está vacía o es null");
            
        Random random = new Random();
        int indice = random.Next(lista.Count);
        return lista[indice];
    }
    
    // Divide una colección en grupos de tamaño específico
    public static IEnumerable<IEnumerable<T>> DividirEnGrupos<T>(
        this IEnumerable<T> coleccion, int tamaño)
    {
        if (tamaño <= 0)
            throw new ArgumentException("El tamaño debe ser mayor que cero");
            
        var lista = coleccion.ToList();
        for (int i = 0; i < lista.Count; i += tamaño)
        {
            yield return lista.Skip(i).Take(tamaño);
        }
    }
    
    // Ejecuta una acción para cada elemento (similar a ForEach de List<T>)
    public static void ParaCadaElemento<T>(this IEnumerable<T> coleccion, Action<T> accion)
    {
        foreach (T elemento in coleccion)
        {
            accion(elemento);
        }
    }
    
    // Encuentra el elemento con el valor máximo según un selector
    public static T MaximoPor<T, TKey>(this IEnumerable<T> coleccion, Func<T, TKey> selector)
        where TKey : IComparable<TKey>
    {
        if (!coleccion.Any())
            throw new InvalidOperationException("La colección está vacía");
            
        T maximo = coleccion.First();
        TKey valorMaximo = selector(maximo);
        
        foreach (T elemento in coleccion.Skip(1))
        {
            TKey valor = selector(elemento);
            if (valor.CompareTo(valorMaximo) > 0)
            {
                maximo = elemento;
                valorMaximo = valor;
            }
        }
        
        return maximo;
    }
}

// Ejemplo de uso con colecciones
class Program
{
    static void Main()
    {
        var numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        var nombres = new List<string> { "Ana", "Luis", "Carlos", "María" };
        
        // Verificar si está vacía
        Console.WriteLine($"¿Números vacía? {numeros.EstaVaciaONull()}");
        
        // Elemento aleatorio
        Console.WriteLine($"Nombre aleatorio: {nombres.ElementoAleatorio()}");
        
        // Dividir en grupos
        Console.WriteLine("Números en grupos de 3:");
        foreach (var grupo in numeros.DividirEnGrupos(3))
        {
            Console.WriteLine($"[{string.Join(", ", grupo)}]");
        }
        
        // Para cada elemento
        Console.WriteLine("Números al cuadrado:");
        numeros.ParaCadaElemento(n => Console.Write($"{n * n} "));
        Console.WriteLine();
        
        // Máximo por longitud
        string nombreMasLargo = nombres.MaximoPor(n => n.Length);
        Console.WriteLine($"Nombre más largo: {nombreMasLargo}");
    }
}

Extensiones avanzadas

Extensiones con múltiples parámetros

Las extensiones pueden recibir parámetros adicionales además del tipo extendido:

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

public static class ExtensionesAvanzadas
{
    // Extensión con parámetros adicionales
    public static bool ContienePalabra(this string texto, string palabra, bool ignorarMayusculas = true)
    {
        if (string.IsNullOrEmpty(texto) || string.IsNullOrEmpty(palabra))
            return false;
            
        StringComparison comparacion = ignorarMayusculas 
            ? StringComparison.OrdinalIgnoreCase 
            : StringComparison.Ordinal;
            
        return texto.IndexOf(palabra, comparacion) >= 0;
    }
    
    // Extensión con parámetro de tipo genérico
    public static IEnumerable<T> FiltrarPor<T>(this IEnumerable<T> coleccion, 
                                               Func<T, bool> predicado, 
                                               int limite = int.MaxValue)
    {
        int contador = 0;
        foreach (T elemento in coleccion)
        {
            if (contador >= limite)
                break;
                
            if (predicado(elemento))
            {
                yield return elemento;
                contador++;
            }
        }
    }
    
    // Extensión que combina múltiples operaciones
    public static string FormatearTexto(this string texto, 
                                       bool capitalizar = false, 
                                       bool eliminarEspaciosExtra = true,
                                       string prefijo = "",
                                       string sufijo = "")
    {
        if (string.IsNullOrEmpty(texto))
            return texto;
            
        string resultado = texto;
        
        if (eliminarEspaciosExtra)
        {
            resultado = resultado.Trim();
            // Reemplazar múltiples espacios con uno solo
            while (resultado.Contains("  "))
            {
                resultado = resultado.Replace("  ", " ");
            }
        }
        
        if (capitalizar)
        {
            resultado = resultado.CapitalizarPalabras(); // Usando extensión anterior
        }
        
        return prefijo + resultado + sufijo;
    }
}

// Ejemplo de uso de extensiones avanzadas
class Program
{
    static void Main()
    {
        string frase = "  hola mundo desde   C#  ";
        
        // Extensión con parámetros
        Console.WriteLine($"¿Contiene 'mundo'? {frase.ContienePalabra("mundo")}");
        Console.WriteLine($"¿Contiene 'MUNDO'? {frase.ContienePalabra("MUNDO", false)}");
        
        // Formateo complejo
        string fraseFormateada = frase.FormatearTexto(
            capitalizar: true,
            eliminarEspaciosExtra: true,
            prefijo: ">>> ",
            sufijo: " <<<"
        );
        Console.WriteLine($"Formateada: '{fraseFormateada}'");
        
        // Filtrado con límite
        var numeros = Enumerable.Range(1, 20);
        var pares = numeros.FiltrarPor(n => n % 2 == 0, limite: 5);
        Console.WriteLine($"Primeros 5 pares: [{string.Join(", ", pares)}]");
    }
}

Extensiones para tipos personalizados

También podemos crear extensiones para nuestros propios tipos:

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

// Clase modelo
public class Producto
{
    public int Id { get; set; }
    public string Nombre { get; set; }
    public decimal Precio { get; set; }
    public string Categoria { get; set; }
    public DateTime FechaCreacion { get; set; }
}

// Extensiones para la clase Producto
public static class ExtensionesProducto
{
    // Verifica si el producto está en oferta (precio menor a un umbral)
    public static bool EstaEnOferta(this Producto producto, decimal umbralOferta = 100m)
    {
        return producto.Precio < umbralOferta;
    }
    
    // Calcula la antigüedad del producto en días
    public static int AntiguedadEnDias(this Producto producto)
    {
        return (DateTime.Now - producto.FechaCreacion).Days;
    }
    
    // Genera una descripción formateada
    public static string GenerarDescripcion(this Producto producto, bool incluirPrecio = true)
    {
        string descripcion = $"{producto.Nombre} - Categoría: {producto.Categoria}";
        
        if (incluirPrecio)
        {
            descripcion += $" - Precio: {producto.Precio:C}";
        }
        
        return descripcion;
    }
}

// Extensiones para colecciones de productos
public static class ExtensionesColeccionProducto
{
    // Filtra productos por categoría
    public static IEnumerable<Producto> PorCategoria(this IEnumerable<Producto> productos, 
                                                     string categoria)
    {
        return productos.Where(p => p.Categoria.Equals(categoria, 
                                   StringComparison.OrdinalIgnoreCase));
    }
    
    // Obtiene productos en un rango de precios
    public static IEnumerable<Producto> EnRangoPrecio(this IEnumerable<Producto> productos,
                                                      decimal minimo, decimal maximo)
    {
        return productos.Where(p => p.Precio >= minimo && p.Precio <= maximo);
    }
    
    // Calcula el precio promedio
    public static decimal PrecioPromedio(this IEnumerable<Producto> productos)
    {
        if (!productos.Any())
            return 0;
            
        return productos.Average(p => p.Precio);
    }
    
    // Agrupa por categoría y cuenta
    public static Dictionary<string, int> ContarPorCategoria(this IEnumerable<Producto> productos)
    {
        return productos.GroupBy(p => p.Categoria)
                       .ToDictionary(g => g.Key, g => g.Count());
    }
}

// Ejemplo de uso con tipos personalizados
class Program
{
    static void Main()
    {
        var productos = new List<Producto>
        {
            new Producto { Id = 1, Nombre = "Laptop", Precio = 899.99m, Categoria = "Tecnología", FechaCreacion = DateTime.Now.AddDays(-30) },
            new Producto { Id = 2, Nombre = "Mesa", Precio = 150.00m, Categoria = "Muebles", FechaCreacion = DateTime.Now.AddDays(-15) },
            new Producto { Id = 3, Nombre = "Ratón", Precio = 25.99m, Categoria = "Tecnología", FechaCreacion = DateTime.Now.AddDays(-5) },
            new Producto { Id = 4, Nombre = "Silla", Precio = 89.99m, Categoria = "Muebles", FechaCreacion = DateTime.Now.AddDays(-45) }
        };
        
        // Usar extensiones de producto individual
        var laptop = productos[0];
        Console.WriteLine($"Descripción: {laptop.GenerarDescripcion()}");
        Console.WriteLine($"¿En oferta? {laptop.EstaEnOferta()}");
        Console.WriteLine($"Antigüedad: {laptop.AntiguedadEnDias()} días");
        Console.WriteLine();
        
        // Usar extensiones de colección
        Console.WriteLine("Productos de tecnología:");
        foreach (var producto in productos.PorCategoria("Tecnología"))
        {
            Console.WriteLine($"- {producto.GenerarDescripcion(false)}");
        }
        Console.WriteLine();
        
        Console.WriteLine("Productos entre 50 y 200:");
        foreach (var producto in productos.EnRangoPrecio(50, 200))
        {
            Console.WriteLine($"- {producto.GenerarDescripcion()}");
        }
        Console.WriteLine();
        
        Console.WriteLine($"Precio promedio: {productos.PrecioPromedio():C}");
        
        Console.WriteLine("Productos por categoría:");
        foreach (var kvp in productos.ContarPorCategoria())
        {
            Console.WriteLine($"- {kvp.Key}: {kvp.Value} producto(s)");
        }
    }
}

LINQ y extensiones de métodos

LINQ (Language Integrated Query) es el ejemplo más conocido de extensiones de métodos en .NET:

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

// Ejemplo mostrando cómo LINQ usa extensiones de métodos
class Program
{
    static void Main()
    {
        var numeros = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // Estas son todas extensiones de métodos de LINQ
        var resultado = numeros
            .Where(n => n % 2 == 0)        // Extensión: filtra pares
            .Select(n => n * n)            // Extensión: eleva al cuadrado
            .OrderByDescending(n => n)      // Extensión: ordena descendente
            .Take(3)                       // Extensión: toma los primeros 3
            .ToList();                     // Extensión: convierte a lista
        
        Console.WriteLine($"Resultado: [{string.Join(", ", resultado)}]");
        
        // Demostración de que son métodos estáticos realmente
        // La línea anterior es equivalente a:
        var resultadoEquivalente = Enumerable.ToList(
            Enumerable.Take(
                Enumerable.OrderByDescending(
                    Enumerable.Select(
                        Enumerable.Where(numeros, n => n % 2 == 0),
                        n => n * n
                    ),
                    n => n
                ),
                3
            )
        );
        
        Console.WriteLine($"Equivalente: [{string.Join(", ", resultadoEquivalente)}]");
    }
}

Mejores prácticas y consideraciones

Directrices de diseño

Aspecto Recomendación
Nombres descriptivos Usa nombres claros que indiquen la funcionalidad
Funcionalidad cohesiva Cada extensión debe tener un propósito específico
Parámetros opcionales Proporciona valores por defecto cuando sea apropiado
Validación Valida parámetros de entrada, especialmente para null
Documentación Documenta el comportamiento y los parámetros

Consideraciones importantes

public static class BuenasPracticasExtensiones
{
    // ✅ BUENA PRÁCTICA: Validación de parámetros
    public static string Truncar(this string texto, int longitud, string sufijo = "...")
    {
        if (texto == null)
            return null;
            
        if (longitud < 0)
            throw new ArgumentException("La longitud no puede ser negativa");
            
        if (texto.Length <= longitud)
            return texto;
            
        return texto.Substring(0, longitud) + sufijo;
    }
    
    // ✅ BUENA PRÁCTICA: Método específico y útil
    public static bool EsNulloVacio(this string texto)
    {
        return string.IsNullOrWhiteSpace(texto);
    }
    
    // ❌ MALA PRÁCTICA: Demasiado genérico
    // public static void HacerAlgo(this object obj) { }
    
    // ✅ BUENA PRÁCTICA: Usar genéricos cuando sea apropiado
    public static T ConValorPorDefecto<T>(this T? valor, T porDefecto) where T : struct
    {
        return valor ?? porDefecto;
    }
}

Resolución de conflictos

Cuando hay métodos con el mismo nombre, C# sigue esta prioridad:

  1. Métodos de instancia del tipo original
  2. Extensiones de métodos en el espacio de nombres actual
  3. Extensiones de métodos en espacios de nombres importados
// Si hay conflicto, se puede llamar explícitamente como método estático
string texto = "ejemplo";
// Llamada como extensión
string resultado1 = texto.Truncar(5);
// Llamada explícita como método estático
string resultado2 = BuenasPracticasExtensiones.Truncar(texto, 5);

Resumen

Las extensiones de métodos son una característica poderosa de C# que permite ampliar la funcionalidad de tipos existentes de manera elegante y legible. Proporcionan la base para LINQ y muchas otras características avanzadas del lenguaje. Su uso apropiado puede hacer que el código sea más expresivo y fácil de leer, especialmente cuando se trabaja con colecciones y operaciones complejas. Sin embargo, deben usarse juiciosamente, siguiendo buenas prácticas de nomenclatura, validación y documentación para crear APIs coherentes y mantenibles.