Compartir a través de


Miembros de extensión (Guía de programación de C#)

Los miembros de extensión permiten "agregar" métodos a tipos existentes sin crear un nuevo tipo derivado, volver a compilar o modificar el tipo original.

A partir de C# 14, hay dos sintaxis que se usan para definir métodos de extensión. C# 14 agrega extension contenedores, donde se definen varios miembros de extensión para un tipo o una instancia de un tipo. Antes de C# 14, agregue el this modificador al primer parámetro de un método estático para indicar que el método aparece como miembro de una instancia del tipo de parámetro.

Los métodos de extensión son métodos estáticos, pero se les llama como si fueran métodos de instancia en el tipo extendido. Para el código de cliente escrito en C#, F# y Visual Basic, no hay ninguna diferencia aparente entre llamar a un método de extensión y los métodos definidos en un tipo. Ambas formas de métodos de extensión se compilan en el mismo IL (lenguaje intermedio). Los consumidores de métodos de extensión no necesitan saber qué sintaxis se usó para definirlos.

Los miembros de extensión más comunes son los operadores de consulta LINQ estándar que agregan funcionalidad de consulta a los tipos existentes System.Collections.IEnumerable y System.Collections.Generic.IEnumerable<T>. Para usar los operadores de consulta estándar, primero debe incluirlos en el ámbito con una using System.Linq directiva . A continuación, cualquier tipo que implemente IEnumerable<T> parece tener métodos de instancia como GroupBy, OrderBy, Average, y así sucesivamente. Puede ver estos métodos adicionales en la finalización de instrucciones de IntelliSense al escribir "punto" después de una instancia de un IEnumerable<T> tipo como List<T> o Array.

Ejemplo de OrderBy

En el ejemplo siguiente se muestra cómo llamar al método del operador OrderBy de consulta estándar en una matriz de enteros. La expresión entre paréntesis es una expresión lambda. Muchos operadores de consulta estándar toman expresiones lambda como parámetros. Para obtener más información, vea Expresiones lambda.

int[] numbers = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = numbers.OrderBy(g => g);
foreach (int i in result)
{
    Console.Write(i + " ");
}
//Output: 10 15 21 26 39 45

Los métodos de extensión se definen como métodos estáticos, pero se llaman mediante la sintaxis del método de instancia. Su primer parámetro especifica sobre qué tipo opera el método. El parámetro sigue el modificador this . Los métodos de extensión solo están en el alcance cuando importas explícitamente el espacio de nombres en el código fuente con una directiva using.

Declarar miembros de extensión

A partir de C# 14, puede declarar bloques de extensión. Un bloque de extensión es un bloque en una clase estática no genérica y no anidada que contiene miembros de extensión para un tipo o una instancia de ese tipo. En el ejemplo de código siguiente se define un bloque de extensión para el string tipo . El bloque de extensión contiene un miembro: un método que cuenta las palabras de la cadena:

namespace CustomExtensionMembers;

public static class MyExtensions
{
    extension(string str)
    {
        public int WordCount() =>
            str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

Antes de C# 14, debe declarar un método de extensión agregando el modificador this al primer parámetro:

namespace CustomExtensionMethods;

public static class MyExtensions
{
    public static int WordCount(this string str) =>
        str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}

Ambas formas de extensiones deben definirse dentro de una clase estática que no esté anidada ni sea genérica.

Y se puede llamar desde una aplicación mediante la sintaxis para acceder a los miembros de instancia:

string s = "Hello Extension Methods";
int i = s.WordCount();

Aunque los miembros de extensión agregan nuevas funcionalidades a un tipo existente, los miembros de extensión no infringen el principio de encapsulación. Las declaraciones de acceso para todos los miembros del tipo extendido se aplican a los miembros de extensión.

Tanto la clase MyExtensions como el método WordCount son static, y se puede acceder a ellos como a todos los demás miembros de static. El WordCount método se puede invocar como otros static métodos de la siguiente manera:

string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);

El código C# anterior se aplica tanto al bloque de extensión como a la sintaxis this para los miembros de extensión. El código anterior:

  • Declara y asigna un nuevo string nombre s con un valor de "Hello Extension Methods".
  • Llama al MyExtensions.WordCount argumento sdado .

Para obtener más información, vea Cómo implementar y llamar a un método de extensión personalizado.

En general, probablemente llames a miembros de extensión mucho más a menudo que los implementas. Dado que se llama a los miembros de extensión como si se declaran como miembros de la clase extendida, no se requiere ningún conocimiento especial para usarlos desde el código de cliente. Para habilitar los miembros de extensión para un tipo determinado, basta con agregar una using directiva para el espacio de nombres en el que se definen los métodos. Por ejemplo, para usar los operadores de consulta estándar, agregue esta using directiva al código:

using System.Linq;

Vincular miembros de extensión a tiempo de compilación

Puede usar miembros de extensión para extender una clase o interfaz, pero no para invalidar el comportamiento definido en una clase. Nunca se llama a un miembro de ampliación con el mismo nombre y firma que los miembros de una interfaz o de una clase. En tiempo de compilación, los miembros de extensión siempre tienen una prioridad menor que los miembros de instancia (o estáticos) definidos en el propio tipo. En otras palabras, si un tipo tiene un método denominado Process(int i)y tiene un método de extensión con la misma firma, el compilador siempre se enlaza al método miembro. Cuando el compilador encuentra una invocación de miembro, primero busca una coincidencia en los miembros del tipo. Si no se encuentra ninguna coincidencia, busca si hay miembros de extensión definidos para el tipo. Enlaza al primer miembro de extensión que encuentra. En el ejemplo siguiente se muestran las reglas que sigue el compilador de C# para determinar si se deben enlazar a un miembro de instancia en el tipo o a un miembro de extensión. La clase Extensions estática contiene miembros de extensión definidos para cualquier tipo que implemente IMyInterface:

public interface IMyInterface
{
    void MethodB();
}

// Define extension methods for IMyInterface.

// The following extension methods can be accessed by instances of any
// class that implements IMyInterface.
public static class Extension
{
    public static void MethodA(this IMyInterface myInterface, int i) =>
        Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

    public static void MethodA(this IMyInterface myInterface, string s) =>
        Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, string s)");

    // This method is never called in ExtensionMethodsDemo1, because each
    // of the three classes A, B, and C implements a method named MethodB
    // that has a matching signature.
    public static void MethodB(this IMyInterface myInterface) =>
        Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
}

Las extensiones equivalentes se pueden declarar mediante la sintaxis de miembro de extensión de C# 14:

public static class Extension
{
    extension(IMyInterface myInterface)
    {
        public void MethodA(int i) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

        public void MethodA(string s) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, string s)");

        // This method is never called in ExtensionMethodsDemo1, because each
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public void MethodB() =>
            Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
    }
}

Las clases A, By C todas implementan la interfaz :

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
class A : IMyInterface
{
    public void MethodB() { Console.WriteLine("A.MethodB()"); }
}

class B : IMyInterface
{
    public void MethodB() { Console.WriteLine("B.MethodB()"); }
    public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}

class C : IMyInterface
{
    public void MethodB() { Console.WriteLine("C.MethodB()"); }
    public void MethodA(object obj)
    {
        Console.WriteLine("C.MethodA(object obj)");
    }
}

No se llama nunca al método de extensión MethodB porque su nombre y signatura coinciden exactamente con los métodos ya implementados por las clases. Cuando el compilador no encuentra un método de instancia con una firma coincidente, se enlaza a un método de extensión coincidente si existe uno.

// Declare an instance of class A, class B, and class C.
A a = new A();
B b = new B();
C c = new C();

// For a, b, and c, call the following methods:
//      -- MethodA with an int argument
//      -- MethodA with a string argument
//      -- MethodB with no argument.

// A contains no MethodA, so each call to MethodA resolves to
// the extension method that has a matching signature.
a.MethodA(1);           // Extension.MethodA(IMyInterface, int)
a.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

// A has a method that matches the signature of the following call
// to MethodB.
a.MethodB();            // A.MethodB()

// B has methods that match the signatures of the following
// method calls.
b.MethodA(1);           // B.MethodA(int)
b.MethodB();            // B.MethodB()

// B has no matching method for the following call, but
// class Extension does.
b.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

// C contains an instance method that matches each of the following
// method calls.
c.MethodA(1);           // C.MethodA(object)
c.MethodA("hello");     // C.MethodA(object)
c.MethodB();            // C.MethodB()
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

Patrones de uso comunes

Funcionalidad de recopilación

En el pasado, era habitual crear "Clases de colección" que implementaban la System.Collections.Generic.IEnumerable<T> interfaz de un tipo determinado y contenían funcionalidad que actuaba en colecciones de ese tipo. Aunque no hay nada malo al crear este tipo de objeto de colección, se puede lograr la misma funcionalidad mediante un uso de una extensión en .System.Collections.Generic.IEnumerable<T> Las extensiones tienen la ventaja de permitir que se llame a la funcionalidad desde cualquier colección, como un System.Array o System.Collections.Generic.List<T> que implemente System.Collections.Generic.IEnumerable<T> en ese tipo. Un ejemplo de esto con una matriz de Int32 se puede encontrar antes en este artículo.

Funcionalidad de Layer-Specific

Al utilizar una Arquitectura de Cebolla u otro diseño de aplicación en capas, es habitual contar con un conjunto de entidades de dominio u objetos de transferencia de datos que se pueden usar para comunicarse a través de los límites de la aplicación. Estos objetos generalmente no contienen ninguna funcionalidad o solo funcionalidad mínima que se aplica a todas las capas de la aplicación. Los métodos de extensión se pueden usar para agregar funcionalidades específicas de cada capa de aplicación.

public class DomainEntity
{
    public int Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{value.FirstName} {value.LastName}";
}

Puede declarar una propiedad equivalente FullName en C# 14 y versiones posteriores mediante la nueva sintaxis de bloque de extensión:

static class DomainEntityExtensions
{
    extension(DomainEntity value)
    {
        string FullName => $"{value.FirstName} {value.LastName}";
    }
}

Extensión de tipos predefinidos

En lugar de crear nuevos objetos cuando es necesario crear la funcionalidad reutilizable, a menudo puede extender un tipo existente, como un tipo .NET o CLR. Por ejemplo, si no usa métodos de extensión, puede crear una Engine clase o Query para realizar el trabajo de ejecutar una consulta en un servidor SQL Server al que se puede llamar desde varios lugares del código. Sin embargo, puede ampliar la System.Data.SqlClient.SqlConnection clase mediante métodos de extensión para realizar esa consulta desde cualquier lugar donde tenga una conexión a sql Server. Otros ejemplos pueden ser agregar funcionalidad común a la System.String clase, ampliar las funcionalidades de procesamiento de datos del System.IO.Stream objeto y System.Exception objetos para una funcionalidad específica de control de errores. Estos tipos de casos de uso solo están limitados por su imaginación y buen sentido.

La extensión de tipos predefinidos puede ser difícil con struct tipos porque se pasan por valor a los métodos. Esto significa que los cambios realizados en la estructura se realizan en una copia de la estructura. Esos cambios no son visibles una vez que se cierra el método de extensión. Puede agregar el ref modificador al primer argumento convirtiéndolo en un ref método de extensión. La ref palabra clave puede aparecer antes o después de la this palabra clave sin ninguna diferencia semántica. Agregar el ref modificador indica que el primer argumento se pasa por referencia. Esta técnica permite escribir métodos de extensión que cambian el estado de la estructura que se extiende (tenga en cuenta que los miembros privados no son accesibles). Solo se permiten tipos de valor o tipos genéricos restringidos a struct (para obtener más información sobre estas reglas, vea struct restricción para obtener más información) como primer parámetro de un ref método de extensión o como receptor de un bloque de extensión. En el ejemplo siguiente se muestra cómo usar un ref método de extensión para modificar directamente un tipo integrado sin necesidad de reasignar el resultado o pasarlo a través de una función con la ref palabra clave :

public static class IntExtensions
{
    public static void Increment(this int number)
        => number++;

    // Take note of the extra ref keyword here
    public static void RefIncrement(this ref int number)
        => number++;
}

Los bloques de extensión equivalentes se muestran en el código siguiente:

public static class IntExtensions
{
    extension(int number)
    {
        public void Increment()
            => number++;
    }

    // Take note of the extra ref keyword here
    extension(ref int number)
    {
        public void RefIncrement()
            => number++;
    }
}

Se requieren diferentes bloques de extensión para distinguir los modos de parámetros por valor y por referencia para el receptor.

Puede ver la diferencia que aplicar ref al receptor tiene en el ejemplo siguiente:

int x = 1;

// Takes x by value leading to the extension method
// Increment modifying its own copy, leaving x unchanged
x.Increment();
Console.WriteLine($"x is now {x}"); // x is now 1

// Takes x by reference leading to the extension method
// RefIncrement changing the value of x directly
x.RefIncrement();
Console.WriteLine($"x is now {x}"); // x is now 2

Puede aplicar la misma técnica agregando ref miembros de extensión a los tipos de estructura definidos por el usuario:

public struct Account
{
    public uint id;
    public float balance;

    private int secret;
}

public static class AccountExtensions
{
    // ref keyword can also appear before the this keyword
    public static void Deposit(ref this Account account, float amount)
    {
        account.balance += amount;

        // The following line results in an error as an extension
        // method is not allowed to access private members
        // account.secret = 1; // CS0122
    }
}

El ejemplo anterior también se puede crear mediante bloques de extensión en C# 14:

public static class AccountExtensions
{
    extension(ref Account account)
    {
        // ref keyword can also appear before the this keyword
        public void Deposit(float amount)
        {
            account.balance += amount;

            // The following line results in an error as an extension
            // method is not allowed to access private members
            // account.secret = 1; // CS0122
        }
    }
}

Puede acceder a estos métodos de extensión de la siguiente manera:

Account account = new()
{
    id = 1,
    balance = 100f
};

Console.WriteLine($"I have ${account.balance}"); // I have $100

account.Deposit(50f);
Console.WriteLine($"I have ${account.balance}"); // I have $150

Directrices generales

Es preferible agregar funcionalidad modificando el código de un objeto o derivando un nuevo tipo siempre que sea razonable y posible hacerlo. Los métodos de extensión son una opción fundamental para crear funcionalidad reutilizable en todo el ecosistema de .NET. Los miembros de extensión son preferibles cuando el origen original no está bajo su control, cuando un objeto derivado es inadecuado o imposible, o cuando la funcionalidad tiene un ámbito limitado.

Para obtener más información sobre los tipos derivados, vea Herencia.

Si implementa métodos de extensión para un tipo determinado, recuerde los siguientes puntos:

  • No se llama a un método de extensión si tiene la misma firma que un método definido en el tipo .
  • Los métodos de extensión se incluyen en el ámbito en el nivel de espacio de nombres. Por ejemplo, si tiene varias clases estáticas que contienen métodos de extensión en un único espacio de nombres denominado Extensions, todos ellos son traídos al ámbito por la directiva using Extensions;.

Para una biblioteca de clases que implementó, no debe usar métodos de extensión para evitar incrementar el número de versión de un ensamblado. Si desea agregar una funcionalidad significativa a una biblioteca para la que posee el código fuente, siga las instrucciones de .NET para el control de versiones de ensamblado. Para obtener más información, consulte Versionado de ensamblados.

Consulte también