Un método de extensión es un método estático especial, definido en una clase estática, pero que se llama como si fuera un método de instancia del tipo que estamos extendiendo.
Imaginad que estáis trabajando con la clase String del sistema. De repente, pensáis: “Ojalá String tuviera un método .WordCount() para contar palabras directamente”.
Pero tenéis un problema: No tenéis el código fuente de String. Es una clase sellada (sealed) de .NET. No podéis heredar de ella ni modificarla.
La solución “antigua” sería crear una clase StringHelper con un método estático:
int palabras = StringHelper.WordCount(miTexto); // Funciona, pero es feo y rompe la fluidez
La solución elegante de C# son los Métodos de Extensión. Nos permiten “inyectar” nuevos métodos a tipos existentes sin modificarlos ni heredar de ellos.
Aunque son muy potentes, tampoco os volváis locos creando extensiones para todo (“Extension Method Hell”).
Sintaxis: El modificador this
Para crear un método de extensión, necesitamos cumplir tres reglas:
- La clase contenedora debe ser
static. - El método debe ser
static. - El primer parámetro del método debe llevar la palabra clave
thisseguida del tipo que queremos extender.
Vamos a implementar el ejemplo del contador de palabras:
using System;
public static class StringExtensions
{
// Fijaos en el 'this string str'
public static int ContarPalabras(this string str)
{
if (string.IsNullOrWhiteSpace(str))
return 0;
return str.Split(new char[] { ' ', '.', '?' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
Ahora cualquier string en nuestro proyecto (siempre que importemos el namespace) tendrá este método disponible:
string miFrase = "Hola mundo, esto es C#";
// Llamada como método de extensión (Fluida y legible)
int n = miFrase.ContarPalabras();
Bajo el capó es syntactic sugar. El compilador nos hace el favor de traducir nuestra llamada bonita a la llamada estática sin que nosotros lo sepamos.
Extendiendo interfaces
Donde los métodos de extensión brillan de verdad no es extendiendo clases concretas, sino extendiendo Interfaces.
Si creas un método de extensión para una interfaz, todas las clases que implementen esa interfaz ganarán esa funcionalidad automáticamente.
Este es el secreto de LINQ. Métodos como .Where(), .Select() o .First() no están definidos en List<T> ni en Array. Son métodos de extensión sobre la interfaz IEnumerable<T>.
Ejemplo: Vamos a crear un método Print() que funcione para cualquier colección.
public static class CollectionExtensions
{
// Extendemos IEnumerable<T>, así que sirve para List, Array, HashSet, etc.
public static void Print<T>(this IEnumerable<T> coleccion)
{
Console.WriteLine("[" + string.Join(", ", coleccion) + "]");
}
}
// Uso
int[] numeros = { 1, 2, 3 };
List<string> nombres = new List<string> { "Luis", "Ana" };
numeros.Print(); // [1, 2, 3]
nombres.Print(); // [Luis, Ana]
Prioridad y conflictos
Es posible que os preguntéis:
¿Qué pasa si creo un método de extensión con el mismo nombre que un método que ya existe en la clase?
C# tiene una regla de prioridad: Los métodos de instancia siempre ganan.
Si la clase String tuviera un método nativo ContarPalabras(), nuestro método de extensión simplemente sería ignorado por el compilador, a menos que lo llamemos de forma estática explícita.
Tened cuidado de no “pisar” nombres futuros. Si Microsoft actualiza .NET y añade un método con el mismo nombre que vuestra extensión, vuestro código dejará de usar vuestra versión silenciosamente y pasará a usar la nativa (que podría comportarse distinto).
Manejo de nulos
A diferencia de los métodos de instancia normales, los métodos de extensión pueden ejecutarse sobre objetos null sin lanzar una excepción inmediata.
Esto ocurre porque, recordad, en realidad es una llamada estática: StringExtensions.Metodo(null).
Esto nos permite crear métodos de chequeo seguros (“Null-safe”):
public static bool IsNullOrEmpty(this string str)
{
return string.IsNullOrEmpty(str);
}
// Uso
string texto = null;
bool vacio = texto.IsNullOrEmpty(); // ¡No explota! Devuelve true.
Si hubiéramos llamado a texto.Length (método de instancia), habría lanzado NullReferenceException.
Ejemplos prácticos
Extender el tipo int
Supongamos que queremos agregar un método al tipo int que devuelva si un número es par:
public static class IntExtensions
{
public static bool EsPar(this int numero)
{
return numero % 2 == 0;
}
}
Uso:
int numero = 10;
if (numero.EsPar())
{
Console.WriteLine($"{numero} es par.");
}
Extender el tipo List<T>
Podemos agregar un método que devuelva el elemento más común en una lista:
public static class ListExtensions
{
public static T ElementoMasComun<T>(this List<T> lista)
{
return lista.GroupBy(x => x)
.OrderByDescending(g => g.Count())
.First()
.Key;
}
}
Uso:
List<int> numeros = new List<int> { 1, 2, 2, 3, 3, 3, 4 };
int masComun = numeros.ElementoMasComun();
Console.WriteLine($"El número más común es: {masComun}");
Extender el tipo DateTime
Podemos agregar un método que calcule la edad a partir de una fecha de nacimiento:
public static class DateTimeExtensions
{
public static int CalcularEdad(this DateTime fechaNacimiento)
{
var hoy = DateTime.Today;
var edad = hoy.Year - fechaNacimiento.Year;
if (fechaNacimiento.Date > hoy.AddYears(-edad)) edad--;
return edad;
}
}
Uso:
DateTime fechaNacimiento = new DateTime(1990, 5, 15);
int edad = fechaNacimiento.CalcularEdad();
Console.WriteLine($"La edad es: {edad} años.");
Método de extensión para IEnumerable<T>
Supongamos que queremos agregar un método que devuelva una cadena con todos los elementos de una colección separados por un delimitador:
public static class EnumerableExtensions
{
public static string UnirConDelimitador<T>(this IEnumerable<T> coleccion, string delimitador)
{
return string.Join(delimitador, coleccion);
}
}
Uso:
List<string> nombres = new List<string> { "Alice", "Bob", "Charlie" };
string resultado = nombres.UnirConDelimitador(", ");
Console.WriteLine(resultado); // Salida: Alice, Bob, Charlie
