Los métodos de extensión son una parte fundamental de C# y la Programación Orientada a Objetos (OOP). Los métodos de extensión en C# te permiten “extender” tipos existentes, incluyendo clases, interfaces o structs, sin modificar su código original.

Esto es particularmente útil cuando deseas agregar nueva funcionalidad a un tipo que no posees o no puedes cambiar, como tipos de bibliotecas de terceros o tipos integrados de .NET como string, List<T>, y así sucesivamente.

En este artículo, aprenderás cómo agregar métodos de extensión a tus clases, así como a clases de terceros y del sistema.

Tabla de Contenidos

Cómo Crear Métodos de Extensión para DateTime

Supongamos que queremos algunos métodos que se puedan usar junto con la clase DateTime existente, tal vez un método que devuelva si el objeto DateTime dado es un fin de semana o algo diferente.

Los métodos de extensión deben definirse en una clase estática porque son esencialmente azúcar sintáctica que te permite llamar a un método estático como si fuera un método de instancia en el tipo que estás extendiendo.

Los métodos de extensión deben estar en una clase estática porque:

  1. No se necesita un objeto: No es necesario crear un objeto para usar un método de extensión. Dado que el método agrega nueva funcionalidad a un tipo existente (como string), puede funcionar sin necesidad de una instancia de la clase.

  2. Código organizado: Colocar métodos de extensión en una clase estática mantiene las cosas ordenadas. Te permite agrupar métodos relacionados y puedes incluirlos fácilmente en tu código utilizando el espacio de nombres adecuado.

Por lo tanto, al usar una clase estática, puedes agregar métodos útiles a tipos existentes sin cambiar su código original, y no necesitas un objeto para llamarlos.

Primero, creemos una clase estática DateTimeExtensions.

public static class DateTimeExtensions {

}

Esto abarcará todas las extensiones de DateTime que queremos crear.

public static bool IsWeekend(this DateTime date)
{
    return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}

Explicación:

public static bool IsWeekend: Esto define que es un método estático llamado IsWeekend que devolverá un valor bool (verdadero/falso).

this DateTime date: La palabra clave this como argumento de un método indica que este método es un método de extensión. Esto significa que el método será una extensión de la clase DateTime.

Cómo encadenar métodos de extensión del mismo tipo

Para que un método de extensión se pueda encadenar con otros, típicamente necesita devolver el mismo tipo que el que está extendiendo (o un tipo compatible). Esto permite llamar a otro método en el resultado del anterior.

using System.Globalization;

public static string ToTitleCase(this string str)
{
    return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower());
}

public static string TrimAndAppend(this string str, string toAppend)
{
    return str.Trim() + toAppend;
}

En el ejemplo anterior, tanto los métodos ToTitleCase como TrimAndAppend devuelven un valor de tipo string, lo que significa que podemos encadenar los métodos de extensión como se muestra a continuación, lo que convertirá la cadena a mayúsculas antes de recortar todos los espacios en blanco y agregar la cadena proporcionada.

Observa que solo proporcionamos el segundo parámetro al método TrimAndAppend, ya que el primer parámetro es la cadena a la que se aplica el método de extensión (como se explicó anteriormente, indicado por la palabra clave this).

var title = "hello world   "
    .ToTitleCase()
    .TrimAndAppend("!!");

//Salida:
// ¡¡Hola Mundo!!

Si el método de extensión devuelve un tipo diferente (que no sea el original o un tipo compatible), no se puede encadenar. Por ejemplo:

var date = new DateTime();
date.IsWeekend().AddDays(1);

Por razones menos obvias, esto no funcionará. Cuando encadenas métodos, no se encadenan desde la variable original, sino desde el tipo de retorno de la llamada al método anterior.

Aquí, tenemos una fecha llamada IsWeekend() que devuelve un valor booleano. Luego intentamos llamar a AddDays(1) en un valor booleano que no existe, ya que es una extensión de DateTime. El compilador de código fallará al compilar, levantando un error que te informa de esto.

Cómo devolver la instancia para encadenar

En algunos métodos de extensión, especialmente aquellos para configuración (como la Inyección de Dependencias), devuelves la misma instancia para permitir el encadenamiento de métodos. Esto te permite seguir trabajando con el objeto original o su estado modificado a través de múltiples llamadas, habilitando una interfaz fluida.

Tomemos el ejemplo de una lista de coches.

public static List<T> RemoveDuplicates<T>(this List<T> list)
{
    // Usa Distinct para eliminar duplicados y actualizar la lista
    list = list.Distinct().ToList();

    // Devuelve la lista modificada para permitir el encadenamiento de métodos
    return list;
}

public static List<T> AddRangeOfItems<T>(this List<T> list, IEnumerable<T> items)
{
    // Agrega un rango de elementos a la lista
    list.AddRange(items);

    // Devuelve la lista modificada para permitir el encadenamiento de métodos
    return list;  
}

Ahora que hemos devuelto la lista de estos métodos de extensión, podemos encadenar métodos adicionales en la misma lista. Por ejemplo, después de eliminar duplicados con RemoveDuplicates(), podemos llamar de inmediato a AddRangeOfItems() en la misma lista.

Así que podemos hacer algo como:

var existingStock = new List<string> { "Ford", "Jaguar", "Ferrari", "Ford", "Renault" };

var availableBrands = existingStock
    .RemoveDuplicates()
    .AddRangeOfItems(new[] { "Lamborghini" }); // nuevo stock disponible

Console.WriteLine("Brands Available Now: " + string.Join(", ", availableBrands));

// Salida: Marcas disponibles ahora: Ford, Jaguar, Ferrari, Renault, Lamborghini

Quitamos duplicados de una lista de marcas de automóviles y añadimos nuevo inventario a la misma lista. Esto funciona porque RemoveDuplicates devuelve la lista, lo que nos permite encadenarlo con AddRangeOfItems.

Si RemoveDuplicates devolviera void en lugar de la lista, no podríamos encadenar los métodos. Aún así se eliminarían los duplicados, pero acciones posteriores como añadir nuevo inventario no serían posibles en la misma expresión.

También tendríamos que actualizar el RemoveDuplicates para actualizar el argumento de lista pasado, ya que Distinct() devuelve una nueva lista que no se está devolviendo como se muestra a continuación, lo cual creo que estarás de acuerdo en que es mucho más verboso.

public static void RemoveDuplicates<T>(this List<T> list)
{
    // Obtener los elementos distintos y limpiar la lista original
    var distinctItems = list.Distinct().ToList();
    list.Clear(); 

    // Añadir los elementos distintos de vuelta a la lista original
    list.AddRange(distinctItems);
}

¿Por qué no puedo simplemente añadir estos métodos a mi clase?

Si el método no es una parte central de la funcionalidad de la clase, colocarlo en un método de extensión puede ayudar a mantener la clase enfocada y mantenible.

Separación de preocupaciones: Usar métodos de extensión mantiene tu código más limpio y ayuda a reducir la complejidad. Ayuda a evitar inflar la clase con métodos que no se utilizan con frecuencia.

Mejorando bibliotecas externas: Si estás utilizando una biblioteca o marco de trabajo donde no puedes modificar el código fuente, los métodos de extensión te permiten añadir funcionalidad a esos tipos sin alterar sus definiciones.

Digamos que estás utilizando la clase FileInfo del espacio de nombres System.IO para trabajar con archivos. Puede que desees añadir un método para verificar fácilmente si un archivo es demasiado grande (por ejemplo, más de 1 GB), pero no puedes modificar directamente la clase FileInfo porque pertenece al espacio de nombres System.IO (es decir, está integrada en .Net).

Sin una extensión:

var fileInfo = new FileInfo("myFile.txt");

if (fileInfo.Length > 1024 * 1024 * 1024) // el tamaño del archivo es mayor a 1GB
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

Con Método de Extensión:

Puedes hacer esto más reutilizable agregando un método de extensión que verifique si el archivo es mayor a 1 GB.

public static class FileInfoExtensions
{
    // método de extensión, con tamaño de archivo predeterminado de 1GB (puede ser reemplazado)
    public static bool IsFileTooLarge(this FileInfo fileInfo, long sizeInBytes = 1024 * 1024 * 1024)
    {
        return fileInfo.Length > sizeInBytes;
    }
}

Ahora puedes utilizar el método IsFileTooLarge directamente en objetos FileInfo, haciendo tu código más limpio:

csharpCopy codevar fileInfo = new FileInfo("myFile.txt");

if (fileInfo.IsFileTooLarge())
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

Extender bibliotecas y paquetes de terceros puede hacer que tu código sea mucho más compatible.

Mejor Organización y Legibilidad: Puedes organizar métodos de extensión en clases estáticas según la funcionalidad o contexto, facilitando encontrarlos y usarlos. Esto ciertamente se mejora al permitir encadenar métodos de extensión.

Cuándo usar extensiones

  • Para Métodos de Utilidad: Si tienes métodos de utilidad útiles para un tipo pero que no pertenecen directamente al tipo en sí (por ejemplo, formato, validación).

  • Para Mejorar Tipos Incorporados: Si deseas agregar funcionalidad a tipos incorporados (como string o DateTime) sin modificarlos.

  • Cuando Quieras Mantener Métodos Opcionales: Si deseas proporcionar métodos adicionales que los usuarios pueden optar por usar sin obligarlos a incorporarlos en el diseño principal de la clase.

Escenario de Ejemplo

Imagina que tienes una clase Person, y deseas agregar un método para formatear el nombre de la persona de manera adecuada:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// Método de extensión en una clase estática
public static class PersonExtensions
{
    public static string GetFullName(this Person person)
    {
        return $"{person.FirstName} {person.LastName}";
    }
}

Al usar un método de extensión para GetFullName, puedes mantener la clase Person simple y enfocada en sus responsabilidades principales, mientras sigues proporcionando funcionalidad útil.

Cuándo No Usar Métodos de Extensión

  • Para Funcionalidad Central: Si un método es esencial para el comportamiento central de una clase, debería ser parte de la propia clase, no una extensión.

  • Para el Acoplamiento Estrecho: Si el método de extensión requiere un conocimiento íntimo del estado privado de la clase o necesita acceso regular a su lógica interna.

  • Para APIs Públicas: Al diseñar una biblioteca o API orientada al público, a menudo es mejor incluir los métodos necesarios directamente en la clase en lugar de forzar a los usuarios a encontrar o crear sus métodos de extensión.

Cositas a Considerar al Diseñar Extensiones

Si bien los métodos de extensión son poderosos y convenientes en muchos casos, hay ciertos inconvenientes o situaciones en las que usarlos podría no ser la mejor opción:

Comportamiento Oculto/Confusión

  • Los métodos de extensión no aparecen directamente en la definición de la clase, lo que significa que pueden ser más difíciles de descubrir por desarrolladores que no están familiarizados con las extensiones disponibles.

  • Los desarrolladores necesitan saber que existen estos métodos de extensión, o podrían pasar por alto su uso a menos que estén trabajando en un IDE con características como IntelliSense (por ejemplo, Visual Studio, JetBrains Rider). Estos IDE pueden sugerir métodos de extensión de otros archivos o espacios de nombres a medida que detectan el tipo apropiado. Sin un IDE rico en funciones, el desarrollador tendría que ser consciente de los métodos de extensión o encontrar la carpeta en la que están almacenados.

No se puede acceder a miembros privados

  • Los métodos de extensión solo pueden acceder a miembros (métodos, propiedades, campos) que sean públicos o internos.

  • No pueden acceder a miembros privados o protegidos de una clase porque los métodos de extensión operan como si fueran parte de la clase desde el exterior, similar a las llamadas de método regulares desde fuera de la clase.

Ejemplo:

public class Car
{
    private string engineNumber = "12345"; // Campo privado

    public string Brand { get; set; } = "Ford"; // Propiedad pública

    private void StartEngine() // Método privado
    {
        Console.WriteLine("Engine started");
    }
}
public static class CarExtensions
{
    public static void DisplayBrand(this Car car)
    {
        Console.WriteLine($"Brand: {car.Brand}"); // Accediendo a la propiedad pública 'Brand'
    }

    public static void TryAccessPrivateField(this Car car)
    {
        // No se puede acceder al 'engineNumber' privado
        // Esto resultará en un error de compilación.
        Console.WriteLine(car.engineNumber);
    }
}

Duplicación y abuso de código

  • En algunos casos, los métodos de extensión pueden fomentar la duplicación de código. Si múltiples proyectos o clases requieren métodos de extensión similares, podrías terminar escribiendo o copiando los mismos métodos de extensión en diferentes lugares, lo que dificulta la gestión y actualización del código de manera consistente.

    Para evitar esto, organiza tu código de manera efectiva. Recomendaría mantener todas las extensiones dentro de una carpeta o proyecto de extensiones, cerca del origen (dependiendo de los patrones de diseño que se estén utilizando en tu aplicación).

  • Abuso de Extensiones: Si se utilizan en exceso, pueden desordenar el espacio global con métodos que quizás no necesiten ser globales. Esto puede causar contaminación en la API del tipo, haciendo más difícil entender qué es esencial para la clase y qué se ha añadido a través de extensiones.

En algunos casos, es mejor encapsular la funcionalidad en clases o servicios auxiliares separados en lugar de añadirla mediante métodos de extensión.

Conclusión

Los métodos de extensión son útiles para añadir funcionalidad de manera limpia y modular, pero también pueden introducir confusión, conflictos de espacio de nombres y falta de acceso a miembros privados.

Como se destacó a lo largo del artículo, tienen muchos usos y son, sin duda, una característica muy interesante del marco Dotnet cuando se utilizan de manera efectiva. Deben usarse cuando sea apropiado, pero no como un sustituto de la funcionalidad que pertenece dentro de la clase misma.