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:
- Métodos de instancia del tipo original
- Extensiones de métodos en el espacio de nombres actual
- 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.