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

Os métodos de extensão permitem que você "adicione" tipos existentes sem criar um novo tipo derivado, recompilar ou, caso contrário, 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. No caso do código cliente gravado em C#, F# e Visual Basic, não há nenhuma 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 System.Collections.IEnumerable e System.Collections.Generic.IEnumerable<T> existentes. Para usar os operadores de consulta padrão, traga-os primeiro ao 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 exibir esses métodos adicionais no preenchimento de declaração do IntelliSense ao digitar "ponto" após uma instância de um tipo IEnumerable<T> como List<T> ou Array.

Exemplo de OrderBy

O exemplo a seguir mostra como chamar o método de consulta padrão OrderBy em qualquer matriz de inteiros. A expressão entre parênteses é uma expressão lambda. Vários operadores de consulta padrão obtêm 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 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 estáticos, mas são chamados usando a sintaxe do método de instância. Seu primeiro parâmetro especifica em qual tipo o método opera. O parâmetro segue o modificador this. Os métodos de extensão só estarão no escopo quando você importar explicitamente o namespace para seu código-fonte com uma diretiva using.

O exemplo a seguir mostra um método de extensão definido para a classe System.String. Isso é definido em 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 método de extensão WordCount pode ser colocado no escopo com esta diretiva using:

using ExtensionMethods;

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

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

Você chama o método de extensão em seu código com a sintaxe do método de instância. A IL (linguagem intermediária) gerada pelo compilador converte 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 classe MyExtensions quanto o método WordCount são static, e podem ser acessados como todos os outros membros static. O método WordCount pode ser invocado como outros métodos static da seguinte maneira:

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

O código anterior do C#:

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

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

Em geral, provavelmente você chamará métodos de extensão com muito mais frequência do que implementará os seus próprios. Como os métodos de extensão são chamados com a sintaxe do método de instância, nenhum conhecimento especial é necessário para usá-los no código do cliente. Para habilitar métodos de extensão para um tipo específico, apenas adicione uma diretiva using para o namespace no qual os métodos estão definidos. Por exemplo, para usar os operadores de consulta padrão, adicione esta diretiva using ao seu código:

using System.Linq;

(Talvez você também precise adicionar uma referência a System.Core.dll.) Você observará que os operadores de consulta padrão agora aparecem no IntelliSense como métodos adicionais disponíveis para a maioria dos tipos IEnumerable<T>.

Associando Métodos de Extensão no Momento da Compilação

Você pode usar métodos de extensão para estender uma classe ou interface, mas não os substituir. Um método de extensão com o mesmo nome e assinatura que um método de interface ou classe nunca será chamado. No tempo de compilação, os métodos de extensão sempre têm menos prioridade que os métodos de instância definidos no próprio tipo. Em outras palavras, se um tipo possuir um método chamado Process(int i) e se 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 invocação de método, primeiro ele procura uma correspondência nos métodos de instância do tipo. Se nenhuma correspondência for encontrada, ele procura quaisquer métodos de extensão definidos para o tipo e vincula-se ao primeiro método de extensão encontrado.

Exemplo

O exemplo a seguir demonstra as regras que o compilador C# segue ao determinar se deve associar uma chamada de método a um método de instância no tipo ou a um método de extensão. A classe estática Extensions contém métodos de extensão definidos para qualquer tipo que implementa IMyInterface. As classes A, B e C implementam a interface.

O método de extensão MethodB nunca é chamado porque seu nome e assinatura são exatamente iguais aos métodos já implementados pelas classes.

Quando o compilador não consegue localizar um método de instância com uma assinatura compatível, ele se associa a um método de extensão correspondente se houver.

// 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 comuns de uso

Funcionalidade de coleção

No passado, era comum criar "Classes de Coleção" que implementavam a interface System.Collections.Generic.IEnumerable<T> para 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 obtida usando uma extensão em System.Collections.Generic.IEnumerable<T>. As extensões têm a vantagem de permitir que a funcionalidade seja chamada de qualquer coleção, como um 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 de camadas tipo cebola 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 funcionalidade ou têm apenas funcionalidades mínimas que se aplica a todas as camadas do aplicativo. Os métodos de extensão podem ser usados para adicionar funcionalidades específicas 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 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 classe Engine ou Query para realizar o trabalho de executar uma consulta em um SQL Server que pode ser chamado de vários lugares do código. No entanto, podemos estender a classe System.Data.SqlClient.SqlConnection 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 funcionalidades comuns à classe System.String, estender os recursos de processamento de dados dos objetos System.IO.Stream e objetos System.Exception para funcionalidades específicas de tratamento de erros. Esses tipos de casos de uso são limitados apenas por sua imaginação e bom senso.

Estender tipos predefinidos pode ser difícil com tipos struct porque eles são passados por valor para os métodos. Isso significa que todas as alterações no struct são feitas em uma cópia do struct. Essas alterações não ficam visíveis quando o método de extensão é encerrado. Você pode adicionar o modificador ref ao primeiro argumento, tornando-o um método de extensão ref. A palavra-chave ref pode aparecer antes ou após a palavra-chave this sem nenhuma diferença semântica. A adição do modificador ref indica que o primeiro argumento é passado por referência. Isso permite escrever 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 ao struct (consulte struct restrição para obter mais informações) são permitidos como o primeiro parâmetro de um método de extensão ref. O exemplo a seguir mostra como utilizar um método de extensão ref para modificar diretamente um tipo interno sem a necessidade de reatribuir o resultado ou passá-lo por uma função com a palavra-chave ref:

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

O próximo exemplo demonstra os métodos de extensão ref para os 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
    }
}

Diretrizes 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 fazer isso, os métodos de extensão se tornaram uma opção crucial para criar funcionalidades reutilizáveis em todo o ecossistema do .NET. Para aquelas ocasiões em que a origem não está sob seu controle, quando um objeto derivado é inadequado ou impossível de usar, ou quando a funcionalidade não deve ser exposta além do escopo aplicável, os métodos de extensão são uma excelente opção.

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

Ao usar um método de extensão para estender um tipo cujo código-fonte você não pode alterar, há o risco de uma alteração na implementação do tipo interromper o funcionamento do método de extensão.

Se você implementar métodos de extensão para um determinado tipo, lembre-se das seguintes considerações:

  • Um método de extensão não é chamado se tiver a mesma assinatura de 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 contenham métodos de extensão em um só namespace chamado Extensions, todos eles serão trazidos para o escopo pela diretiva using Extensions;.

Para uma biblioteca de classes que você implemente, não use métodos de extensão para evitar incrementar o número de versão de um assembly. Se desejar adicionar funcionalidade significativa a uma biblioteca da qual você tenha o código-fonte, siga as diretrizes do .NET para controle de versão do assembly. Para obter mais informações, consulte Controle de versão do assembly.

Confira também