Métodos de extensão (Guia de programação em C#)

Os métodos de extensão permitem que você "adicione" métodos a tipos existentes sem criar um novo tipo derivado, recompilar ou modificar o tipo original. 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.

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

OrderBy Exemplo

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, mas isso não é um requisito para métodos de extensão. Para obter mais informações, consulte Expressões do Lambda.

class ExtensionMethods2
{

    static void Main()
    {
        int[] ints = [10, 45, 15, 39, 21, 26];
        var result = ints.OrderBy(g => g);
        foreach (var i in result)
        {
            System.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 você importa explicitamente o namespace para o código-fonte com uma using diretiva.

O exemplo a seguir mostra um método de extensão definido para a System.String classe. Ele é definido dentro de uma classe estática não aninhada e não genérica:

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

O WordCount método de extensão pode ser introduzido com a presente using diretiva:

using ExtensionMethods;

E ele pode ser chamado a partir de um aplicativo usando esta sintaxe:

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

Você invoca o método de extensão em seu código com sintaxe de método de instância. A linguagem intermediária (IL) gerada pelo compilador traduz seu código em uma chamada no método estático. O princípio do encapsulamento não está realmente sendo violado. Os métodos de extensão não podem acessar variáveis privadas no tipo que estão estendendo.

Tanto a MyExtensions classe quanto o método são static, e ele pode ser acessado WordCount 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:

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

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

Em geral, você provavelmente estará chamando métodos de extensão com muito mais frequência do que implementando os seus próprios. Como os métodos de extensão são chamados usando a sintaxe do método de instância, nenhum conhecimento especial é necessário para usá-los a partir do código do cliente. Para habilitar métodos 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;

(Também pode ser necessário adicionar uma referência a System.Core.dll.) Você notará que os operadores de consulta padrão agora aparecem no IntelliSense como métodos adicionais disponíveis para a maioria dos IEnumerable<T> tipos.

Métodos de extensão de vinculação em tempo de compilação

Você pode usar métodos de extensão para estender uma classe ou interface, mas não para substituí-los. Um método de extensão com o mesmo nome e assinatura que um método de interface ou classe nunca será chamado. Em tempo de compilação, os métodos de extensão sempre têm prioridade menor do que os métodos de instância definidos no próprio tipo. Em outras palavras, se um tipo tiver um método chamado Process(int i), e você tiver um método de extensão com a mesma assinatura, o compilador sempre se associará ao método de instância. Quando o compilador encontra uma chamada de método, ele primeiro procura uma correspondência nos métodos de instância do tipo. Se nenhuma correspondência for encontrada, ele procurará quaisquer métodos de extensão definidos para o tipo e vinculará ao primeiro método de extensão encontrado.

Exemplo

O exemplo a seguir demonstra as regras que o compilador C# segue para determinar se uma chamada de método deve ser vinculada a um método de instância no tipo ou a um método de extensão. A classe Extensions static contém métodos de extensão definidos para qualquer tipo que implemente IMyInterfaceo . Classes A, Be C todos implementam a interface.

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 será vinculado a um método de extensão correspondente, se existir.

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    public interface IMyInterface
    {
        // Any class that implements IMyInterface must define a method
        // that matches the following signature.
        void MethodB();
    }
}

// Define extension methods for IMyInterface.
namespace Extensions
{
    using System;
    using DefineIMyInterface;

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

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
    using System;
    using Extensions;
    using DefineIMyInterface;

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

    class ExtMethodDemo
    {
        static void Main(string[] args)
        {
            // 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 uso 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 específica da camada

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 sem carregar o objeto com métodos não necessários ou desejados em outras camadas.

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

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

Estendendo tipos predefinidos

Em vez de criar novos objetos quando a funcionalidade reutilizável precisa ser criada, muitas vezes podemos estender um tipo existente, como um tipo .NET ou CLR. Por exemplo, se não usarmos métodos de extensão, podemos 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, podemos estender a System.Data.SqlClient.SqlConnection classe usando métodos de extensão para executar essa consulta de qualquer lugar que tenhamos 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.

Estender tipos predefinidos pode ser difícil com struct tipos porque eles são passados por valor para 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. Isso 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 estão acessíveis). Somente tipos de valor ou tipos genéricos restritos a struct (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. 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++;
}

public static class IntProgram
{
    public static void Test()
    {
        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
    }
}

Este próximo exemplo demonstra ref métodos de extensão para tipos struct definidos pelo usuário:

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

public static class AccountProgram
{
    public static void Test()
    {
        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

Embora ainda seja considerado 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 tornaram-se uma opção crucial para criar funcionalidade reutilizável em todo o ecossistema .NET. Para as ocasiões em que a fonte original não está sob seu controle, quando um objeto derivado é inadequado ou impossível, ou quando a funcionalidade não deve ser exposta além de seu escopo aplicável, os métodos de extensão são uma excelente escolha.

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

Ao usar um método de extensão para estender um tipo cujo código-fonte você não está no controle, você corre o risco de que uma alteração na implementação do tipo faça com que seu método de extensão seja interrompido.

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 trazidos para o escopo no 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.

Consulte também