
Los métodos extensores aparecieron con C# 3.0, hace casi veinte años. Desde entonces, se han convertido en una herramienta muy útil para los desarrolladores de C#, permitiéndonos "añadir" métodos a tipos existentes sin modificar su código fuente ni crear un nuevo tipo derivado. Sin embargo, desde aquella lejana versión, este mecanismo no había sufrido cambios significativos.
Con la llegada de C# 14 y .NET 10, el concepto de extensión se ha hecho mucho más ambicioso. Como veremos a continuación, no solo es posible definir métodos extensores, sino también propiedades y otros tipos de miembros, tanto de instancia como estáticos. Además, la sintaxis se ha mejorado para hacerla más clara y fomentar la cohesión entre los distintos miembros de extensión que implementemos para un mismo tipo de datos.
Lo vemos a continuación.
Los bloques extension
Aunque a nivel de lenguaje se mantiene la sintaxis clásica para definir métodos extensores, ahora es posible utilizar la palabra clave extension
para delimitar un bloque de código que contiene miembros de extensión asociados a un mismo tipo de datos.
Vamos a verlo con un ejemplo. El siguiente código define un par de métodos extensores para la clase string
, usando la sintaxis clásica:
public static class StringExtensions
{
public static bool IsCapitalized(this string str)
{
return !string.IsNullOrEmpty(str) && char.IsUpper(str[0]);
}
public static string Truncate(this string str, int maxLength)
{
if (string.IsNullOrEmpty(str) || maxLength <= 0)
return string.Empty;
return str.Length <= maxLength ? str : str.Substring(0, maxLength);
}
}
Con la nueva sintaxis, podemos escribir lo mismo de la siguiente manera:
public static class StringExtensions
{
extension(string str)
{
public bool IsCapitalized()
{
return !string.IsNullOrEmpty(str) && char.IsUpper(str[0]);
}
public string Truncate(int maxLength)
{
if (string.IsNullOrEmpty(str) || maxLength <= 0)
return string.Empty;
return str.Length <= maxLength ? str : str.Substring(0, maxLength);
}
}
}
Fijaos que el cuerpo de los métodos no ha cambiado, ni tampoco la clase estática que lo envuelve todo. Sin embargo, encontramos la palabra clave extension
, seguida de un parámetro del tipo de dato al que vamos a añadir los extensores, delimitando un bloque de código que contiene los miembros de extensión asociados a dicho tipo. En su interior, los métodos ya no necesitan el modificador static
, ni especificar el parámetro this
con el tipo extendido: usarán el parámetro definido en el bloque extension
.
Los bloques extension
solo pueden definirse en el interior de clases estáticas de primer nivel (es decir, no anidadas) y que no usen parámetros genéricos. En una clase de este tipo podemos incluir tantos bloques extension
como deseemos, incluso aplicables a distintos tipos de datos; además, dado que esta nueva funcionalidad es compatible con la sintaxis clásica, podemos mezclar ambos estilos en la misma clase estática, como en el siguiente ejemplo:
public static class SharedExtensions
{
// Usamos la sintaxis clásica
public static bool IsCapitalized(this string str) { ... }
// Usamos el bloque extension para el tipo string
extension(string str)
{
public string Truncate(int maxLength) { ... }
}
// Otro bloque para extensiones del tipo int
extension(int number)
{
public bool IsEven() => number % 2 == 0;
}
}
Y a estas alturas, probablemente os preguntaréis por qué esta sintaxis era necesaria, si ya teníamos la sintaxis clásica que permite hacer lo mismo 🤔
Pues bien, aparte de poder agrupar los extensores por tipo de dato, que podría mejorar la legibilidad y ayudar a organizar mejor el código, lo que conseguimos con esta nueva sintaxis es separar la especificación del tipo extendido del resto de parámetros y, como consecuencia, permitir la entrada de nuevos tipos de miembros extensores.
Por fin, ¡propiedades extensoras!
Con la sintaxis clásica no podíamos definir propiedades extensoras, porque no había forma de especificar el tipo de dato a extender en la declaración de la propiedad, como sí hacemos con el parámetro this
de los métodos. Con la nueva sintaxis, podemos conseguirlo fácilmente, dado que el tipo extendido ya está definido en el bloque extension
:
public static class StringExtensions
{
extension(string str)
{
public char? FirstCharacter
{
get => string.IsNullOrEmpty(str) ? null : str[0];
}
}
}
El código anterior nos permite utilizar la propiedad FirstCharacter
como si fuera una propiedad de instancia de la clase string
:
string name = "John";
Console.WriteLine(name.FirstCharacter); // Imprime 'J'
Aunque probablemente en la mayoría de escenarios las propiedades extensoras serán de solo lectura, también es posible definir propiedades con un setter, como en el siguiente ejemplo, donde añadimos a la clase Friend
, que únicamente incluye las propiedades FirstName
y LastName
, una propiedad FullName
que permite establecer y obtener los nombres y apellidos como una cadena completa:
public class Friend
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public static class FriendExtensions
{
extension(Friend friend)
{
public string FullName
{
get => $"{friend.FirstName} {friend.LastName}";
set
{
var indexOfSpace = value.IndexOf(' ');
if (indexOfSpace > 0)
{
friend.FirstName = value[..indexOfSpace];
friend.LastName = value[(indexOfSpace + 1)..].Trim();
}
else
{
friend.FirstName = value;
friend.LastName = string.Empty;
}
}
}
}
}
Una vez hecho, podríamos usar esta nueva propiedad "virtual" así:
var clark = new Friend();
clark.FullName = "Clark Kent";
var peter = new Friend();
peter.FirstName = "Peter";
peter.LastName = "Parker";
Console.WriteLine(clark.FullName); // Muestra "Clark Kent"
Console.WriteLine(peter.FullName); // Muestra "Peter Parker"
Extensores estáticos
Además de los miembros de instancia, ahora también es posible definir miembros extensores sobre clases estáticas, que se comportan como miembros estáticos normales, pero asociados a un tipo específico. Por ejemplo, podríamos definir un método extensor estático para la clase Math
que calcule el factorial de un número:
public static class MathExtensions
{
extension(Math)
{
public static int Factorial(int n)
{
if (n < 0) throw new ArgumentException("Invalid negative value", nameof(n));
return n == 0 ? 1 : n * Factorial(n - 1);
}
}
}
Fijaos que, en este caso, la forma de definir el bloque extension
cambia ligeramente, ya que, al tratarse de miembros estáticos, solo necesitamos especificar el tipo de datos sobre el que se van a definir los extensores, sin necesidad de definir un nombre de parámetro.
Igualmente, podríamos definir propiedades estáticas como la siguiente, a la que podremos acceder usando la expresión Math.GoldenRatio
:
public static class MathExtensions
{
extension(Math)
{
public static double GoldenRatio => 1.618033988749895; // Número áureo
}
}
Una aplicación concreta de este tipo de extensores estáticos podría ser la definición de operadores sobre cualquier tipo. Por ejemplo, podríamos definir un extensor para la clase string
que permita replicar una cadena usando el operador de multiplicación:
Console.WriteLine("Hello" * 3); // Imprime "HelloHelloHello"
public static class StringExtensions
{
extension(string)
{
public static string operator *(string str, int times)
{
if (times <= 0) return string.Empty;
return string.Concat(Enumerable.Repeat(str, times));
}
}
}
Desambiguación de miembros extensores
Los miembros extensores tienen una precedencia menor que los miembros de instancia, lo que puede llevar a situaciones de ambigüedad si un tipo tiene un miembro de instancia con el mismo nombre y firma que un miembro extensor. En estos casos, el compilador siempre preferirá el miembro de instancia.
Si queremos forzar el uso de un miembro extensor en lugar de un miembro de instancia, podemos hacerlo accediendo directamente al método estático que lo define. Podemos recrear fácilmente un escenario como el siguiente, donde vemos un conjunto de extensiones maquiavélicas que intentarán confundirnos al reescribir métodos o propiedades que ya existen en la clase string
:
public static class EvilStringExtensions
{
extension(string str)
{
public string ToUpper() => str.ToLower();
public int Length => Random.Shared.Next(1, str.Length + 1);
}
}
Dado que por defecto se usarán los miembros de instancia, si quisiéramos forzar el uso de los extensores definidos en la clase EvilStringExtensions
, tendríamos que hacerlo de forma explícita, accediendo a ellos como métodos estáticos de la clase EvilStringExtensions
:
string s = "Hello";
// Por defecto se usa el método de instancia:
Console.WriteLine(s.ToUpper()); // Muestra "HELLO"
// Forzamos el uso del extensor a través de la clase estática que lo contiene:
Console.WriteLine(EvilStringExtensions.ToUpper(s)); // Muestra "hello"
En el caso de las propiedades extensoras, el problema es similar, aunque en este caso no podemos acceder a ellas como propiedades de instancia, sino que tendremos que usar los métodos get
y set
generados automáticamente por el compilador. Fijaos que por cada propiedad y accesor (sea getter o setter) se genera un método estático con el prefijo get_
o set_
, respectivamente:
string s = "Hello";
// Por defecto se usa la propiedad de instancia:
Console.WriteLine(s.Length); // Muestra 5
// Forzamos el uso del extensor a través de la clase estática que lo contiene:
Console.WriteLine(EvilStringExtensions.get_Length(s)); // Muestra un número aleatorio
Conclusión
Los bloques extension
y los nuevos tipos de miembros extensores en C# 14 y .NET 10 representan una evolución significativa en la forma en que podemos extender la funcionalidad de los tipos existentes. Esta nueva sintaxis no solo puede mejorar la organización y legibilidad del código, sino que también abre la puerta a nuevas posibilidades, como la definición de propiedades y miembros estáticos extensores.
Además, su compatibilidad con la sintaxis clásica asegura que los desarrolladores podamos ir adoptando estas nuevas características de manera gradual, aprovechando al máximo las ventajas que ofrecen sin necesidad de reescribir código existente.
Publicado en Variable not found.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario