Partilhar via


Membros de extensão (Guia de Programação em C#)

Os membros de extensão permitem que você "adicione" métodos a tipos existentes sem criar um novo tipo derivado, recompilar ou modificar o tipo original.

A partir do C# 14, há duas sintaxes que você usa para definir métodos de extensão. O C# 14 adiciona extension contêineres, onde você define vários membros de extensão para um tipo ou uma instância de um tipo. Antes do C# 14, você adiciona o this modificador ao primeiro parâmetro de um método estático para indicar que o método aparece como membro de uma instância do tipo de parâmetro.

Os métodos de extensão são métodos estáticos, mas são chamados como se fossem métodos de instância no tipo estendido. Para código de cliente escrito em C#, F# e Visual Basic, não há diferença aparente entre chamar um método de extensão e os métodos definidos em um tipo. Ambas as formas de métodos de extensão são compilados para a mesma IL (Intermediate Language). Os utilizadores de membros de extensão não precisam saber que sintaxe foi usada para definir métodos de extensão.

Os membros de extensão mais comuns são os operadores de consulta padrão LINQ que adicionam funcionalidade de consulta aos tipos existentes System.Collections.IEnumerable e System.Collections.Generic.IEnumerable<T>. Para usar os operadores de consulta padrão, primeiro traga-os para o escopo com uma diretiva using System.Linq. Em seguida, qualquer tipo que implemente IEnumerable<T> parece ter métodos de instância como GroupBy, OrderBy, Average e assim por diante. Você pode ver esses métodos extras no preenchimento da instrução IntelliSense quando digita "ponto" após uma instância de um IEnumerable<T> tipo como List<T> ou Array.

Exemplo de OrderBy

O exemplo a seguir mostra como chamar o método de operador OrderBy de consulta padrão em uma matriz de inteiros. A expressão entre parênteses é uma expressão lambda. Muitos operadores de consulta padrão usam expressões lambda como parâmetros. Para obter mais informações, consulte Expressões do 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

Os métodos de extensão são definidos como métodos estáticos, mas são chamados usando a sintaxe do método de instância. O primeiro parâmetro especifica em que tipo o método opera. O parâmetro segue o modificador this . Os métodos de extensão só estão no escopo quando importas explicitamente o namespace no teu código-fonte com uma diretiva using.

Declarar membros de extensão

A partir do C# 14, você pode declarar blocos de extensão. Um bloco de extensão é um bloco em uma classe estática não aninhada, não genérica que contém membros de extensão para um tipo ou uma instância desse tipo. O exemplo de código a seguir define um bloco de extensão para o string tipo. O bloco de extensão contém um membro: um método que conta as palavras na cadeia de caracteres:

namespace CustomExtensionMembers;

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

Antes do C# 14, você declara um método de extensão adicionando o this modificador ao primeiro parâmetro:

namespace CustomExtensionMethods;

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

Ambas as formas de extensões devem ser definidas dentro de uma classe estática não aninhada e não genérica.

E ele pode ser chamado a partir de um aplicativo usando a sintaxe para acessar membros da instância:

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

Embora os membros da extensão adicionem novos recursos a um tipo existente, os membros da extensão não violam o princípio do encapsulamento. As declarações de acesso para todos os membros do tipo estendido aplicam-se aos membros da extensão.

Tanto a MyExtensions classe quanto o WordCount método são static, e pode ser acedido como todos os outros static membros. O WordCount método pode ser invocado como outros static métodos da seguinte maneira:

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

O código C# anterior aplica-se tanto ao bloco de extensão como à sintaxe this para membros de extensão. O código anterior:

  • Declara e atribui um novo string nome s com um valor de "Hello Extension Methods".
  • Chama o argumento dado MyExtensions.WordCounts.

Para obter mais informações, consulte Como implementar e chamar um método de extensão personalizado.

Em geral, você provavelmente chama membros de extensão com muito mais frequência do que os implementa. Como os membros da extensão são chamados como se fossem declarados como membros da classe estendida, nenhum conhecimento especial é necessário para usá-los a partir do código do cliente. Para habilitar membros de extensão para um tipo específico, basta adicionar uma using diretiva para o namespace no qual os métodos são definidos. Por exemplo, para usar os operadores de consulta padrão, adicione esta using diretiva ao seu código:

using System.Linq;

Vinculando membros de extensão em tempo de compilação

Você pode usar membros de extensão para estender uma classe ou interface, mas não para substituir o comportamento definido em uma classe. Um membro de extensão com o mesmo nome e assinatura de um membro de interface ou classe nunca é chamado. Em tempo de compilação, os membros da extensão sempre têm prioridade menor do que os membros da instância (ou estáticos) definidos no próprio tipo. Em outras palavras, se um tipo tem um método chamado Process(int i), e você tem um método de extensão com a mesma assinatura, o compilador sempre se liga ao método membro. Quando o compilador encontra uma invocação de membro, primeiro procura uma correspondência nos membros do tipo. Se nenhuma correspondência for encontrada, ele procurará por qualquer membro de extensão definido para o tipo. Liga-se ao primeiro membro da extensão que encontrar. O exemplo a seguir demonstra as regras que o compilador C# segue para determinar se deve ser vinculado a um membro da instância no tipo ou a um membro da extensão. A classe Extensions static contém membros de extensão definidos para qualquer 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)");
}

As extensões equivalentes podem ser declaradas usando a sintaxe de membro da extensão 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)");
    }
}

Classes A, Be C todos implementam a interface:

// 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)");
    }
}

O MethodB método de extensão nunca é chamado porque seu nome e assinatura correspondem exatamente aos métodos já implementados pelas classes. Quando o compilador não consegue encontrar um método de instância com uma assinatura correspondente, ele se liga a um método de extensão correspondente, se existir.

// 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()
 */

Padrões de utilização comuns

Funcionalidade da Coleção

No passado, era comum criar "Classes de Coleção" que implementavam a System.Collections.Generic.IEnumerable<T> interface para um determinado tipo e continham funcionalidades que atuavam em coleções desse tipo. Embora não haja nada de errado em criar esse tipo de objeto de coleção, a mesma funcionalidade pode ser alcançada usando uma extensão no System.Collections.Generic.IEnumerable<T>. As extensões têm a vantagem de permitir que a funcionalidade seja chamada a partir de qualquer coleção, como uma System.Array ou System.Collections.Generic.List<T> que implementa System.Collections.Generic.IEnumerable<T> nesse tipo. Um exemplo disso usando uma matriz de Int32 pode ser encontrado anteriormente neste artigo.

Funcionalidade Layer-Specific

Ao usar uma arquitetura Onion ou outro design de aplicativo em camadas, é comum ter um conjunto de entidades de domínio ou objetos de transferência de dados que podem ser usados para se comunicar entre os limites do aplicativo. Esses objetos geralmente não contêm nenhuma funcionalidade ou apenas funcionalidade mínima que se aplica a todas as camadas do aplicativo. Os métodos de extensão podem ser usados para adicionar funcionalidade específica a cada camada de aplicativo.

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}";
}

Você pode declarar uma propriedade equivalente FullName em C# 14 e posterior usando a nova sintaxe de bloco de extensão:

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

Estendendo tipos predefinidos

Em vez de criar novos objetos quando a funcionalidade reutilizável precisa ser criada, muitas vezes você pode estender um tipo existente, como um tipo .NET ou CLR. Por exemplo, se você não usar métodos de extensão, poderá criar uma Engine classe ou Query para executar uma consulta em um SQL Server que pode ser chamada de vários locais em nosso código. No entanto, você pode, em vez disso, estender a System.Data.SqlClient.SqlConnection classe usando métodos de extensão para executar essa consulta de qualquer lugar que você tenha uma conexão com um SQL Server. Outros exemplos podem ser adicionar funcionalidade comum à System.String classe, estender os recursos de processamento de dados do System.IO.Stream objeto e System.Exception objetos para funcionalidade específica de tratamento de erros. Estes tipos de casos de uso são limitados apenas pela sua imaginação e bom senso.

A extensão de tipos predefinidos pode ser difícil com tipos struct, porque são passados por valor para os métodos. Isso significa que quaisquer alterações na estrutura são feitas em uma cópia da estrutura. Essas alterações não são visíveis quando o método de extensão é encerrado. Você pode adicionar o ref modificador ao primeiro argumento, tornando-o um ref método de extensão. A ref palavra-chave pode aparecer antes ou depois da this palavra-chave sem quaisquer diferenças semânticas. Adicionar o ref modificador indica que o primeiro argumento é passado por referência. Essa técnica permite que você escreva métodos de extensão que alteram o estado da estrutura que está sendo estendida (observe que os membros privados não são acessíveis). Somente tipos de valor ou tipos genéricos restritos a estruturar (Para obter mais informações sobre essas regras, consulte struct restrição para obter mais informações) são permitidos como o primeiro parâmetro de um ref método de extensão ou como o recetor de um bloco de extensão. O exemplo a seguir mostra como usar um ref método de extensão para modificar diretamente um tipo interno sem a necessidade de reatribuir o resultado ou passá-lo por uma função com a ref palavra-chave:

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++;
}

Os blocos de extensão equivalentes são mostrados no seguinte código:

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++;
    }
}

São necessários blocos de extensão distintos para distinguir os modos de parâmetro por valor e por referência para o recetor.

Você pode ver a diferença que a aplicação de ref ao recetor faz no seguinte exemplo:

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

Pode aplicar a mesma técnica ao adicionar membros de extensão ref a tipos de estrutura definidos pelo utilizador.

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
    }
}

O exemplo anterior também pode ser criado usando blocos de extensão em 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
        }
    }
}

Você pode acessar esses métodos de extensão da seguinte maneira:

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

Orientações Gerais

É preferível adicionar funcionalidade modificando o código de um objeto ou derivando um novo tipo sempre que for razoável e possível fazê-lo. Os métodos de extensão são uma opção crucial para criar funcionalidade reutilizável em todo o ecossistema .NET. Os membros da extensão são preferíveis quando a fonte original não está sob seu controle, quando um objeto derivado é inadequado ou impossível ou quando a funcionalidade tem escopo limitado.

Para obter mais informações sobre tipos derivados, consulte Herança.

Se você implementar métodos de extensão para um determinado tipo, lembre-se dos seguintes pontos:

  • Um método de extensão não é chamado se tiver a mesma assinatura que um método definido no tipo.
  • Os métodos de extensão são introduzidos no âmbito ao nível do namespace. Por exemplo, se você tiver várias classes estáticas que contêm métodos de extensão em um único namespace chamado Extensions, todas elas serão incluídas no escopo pela using Extensions; diretiva.

Para uma biblioteca de classes que você implementou, você não deve usar métodos de extensão para evitar incrementar o número de versão de um assembly. Se você quiser adicionar funcionalidade significativa a uma biblioteca para a qual você possui o código-fonte, siga as diretrizes do .NET para controle de versão de assembly. Para obter mais informações, consulte Assembly Versioning.

Ver também