Métodos de extensão (Visual Basic)

Os métodos de extensão permitem que os desenvolvedores adicionem funcionalidade personalizada aos tipos de dados que já estão definidos sem criar um tipo derivado. Os métodos de extensão possibilitam a gravação de um método que pode ser chamado como se fosse um método de instância do tipo existente.

Comentários

Um método de extensão pode ser apenas um procedimento Sub ou um procedimento Function. Você não pode definir uma propriedade um campo ou um evento de extensão. Todos os métodos de extensão precisam ser marcados com o atributo de extensão <Extension> do namespace System.Runtime.CompilerServices e precisam ser definidos em um Módulo. Se um método de extensão for definido fora de um módulo, o compilador do Visual Basic gerará o erro BC36551, "Métodos de extensão só podem ser definidos em módulos".

O primeiro parâmetro em uma definição de método de extensão especifica qual tipo de dados o método estende. Quando o método é executado, o primeiro parâmetro é associado à instância do tipo de dados que invoca o método.

O atributo Extension só pode ser aplicado a um Module, Sub ou Function do Visual Basic. Se você o aplicar a um Class ou um Structure, o compilador do Visual Basic gerará o erro BC36550, "O atributo 'Extension' só pode ser aplicado às declarações 'Module', 'Sub' ou 'Function'".

Exemplo

O exemplo a seguir define uma extensão Print para o tipo de dados String. O método usa Console.WriteLine para exibir uma cadeia de caracteres. O parâmetro do método Print, aString, estabelece que o método estende a classe String.

Imports System.Runtime.CompilerServices

Module StringExtensions

    <Extension()> 
    Public Sub Print(ByVal aString As String)
        Console.WriteLine(aString)
    End Sub

End Module

Observe que a definição do método de extensão está marcada com o atributo de extensão <Extension()>. Marcar o módulo no qual o método é definido é opcional, mas cada método de extensão precisa ser marcado. System.Runtime.CompilerServices precisa ser importado para acessar o atributo de extensão.

Os métodos de extensão podem ser declarados apenas em módulos. Normalmente, o módulo no qual um método de extensão é definido não é o mesmo módulo do qual ele é chamado. Em vez disso, o módulo que contém o método de extensão é importado, se necessário, para colocá-lo no escopo. Depois que o módulo que contém Print estiver no escopo, o método pode ser chamado como se fosse um método de instância comum que não usa argumentos, como ToUpper:

Module Class1

    Sub Main()

        Dim example As String = "Hello"
        ' Call to extension method Print.
        example.Print()

        ' Call to instance method ToUpper.
        example.ToUpper()
        example.ToUpper.Print()

    End Sub

End Module

O próximo exemplo, PrintAndPunctuate, também é uma extensão para String, desta vez definida com dois parâmetros. O primeiro parâmetro, aString, estabelece que o método de extensão se estende String. O segundo parâmetro, punc, destina-se a ser uma cadeia de caracteres de marcas de pontuação que é passada como um argumento quando o método é chamado. O método exibe a cadeia de caracteres seguida pelas marcas de pontuação.

<Extension()> 
Public Sub PrintAndPunctuate(ByVal aString As String, 
                             ByVal punc As String)
    Console.WriteLine(aString & punc)
End Sub

O método é chamado enviando um argumento de cadeia de caracteres para punc: example.PrintAndPunctuate(".")

O exemplo a seguir mostra Print e PrintAndPunctuate definidos e chamados. System.Runtime.CompilerServices é importado no módulo de definição para habilitar o acesso ao atributo de extensão.

Imports System.Runtime.CompilerServices

Module StringExtensions

    <Extension()>
    Public Sub Print(aString As String)
        Console.WriteLine(aString)
    End Sub

    <Extension()>
    Public Sub PrintAndPunctuate(aString As String, punc As String)
        Console.WriteLine(aString & punc)
    End Sub
End Module

Em seguida, os métodos de extensão são colocados no escopo e chamados:

Imports ConsoleApplication2.StringExtensions

Module Module1

    Sub Main()
        Dim example As String = "Example string"
        example.Print()

        example = "Hello"
        example.PrintAndPunctuate(".")
        example.PrintAndPunctuate("!!!!")
    End Sub
End Module

Tudo o que é necessário para ser capaz de executar esses ou métodos de extensão semelhantes é que eles estejam no escopo. Se o módulo que contém um método de extensão estiver no escopo, ele estará visível no IntelliSense e poderá ser chamado como se fosse um método de instância comum.

Observe que, quando os métodos são invocados, nenhum argumento é enviado para o primeiro parâmetro. O parâmetro aString nas definições de método anteriores está associado à example, a instância de String que as chama. O compilador usará example como o argumento enviado para o primeiro parâmetro.

Se um método de extensão for chamado para um objeto definido como Nothing, o método de extensão será executado. Isso não se aplica aos métodos de instância comuns. Você pode verificar Nothing explicitamente no método de extensão.

Tipos que podem ser estendidos

Você pode definir um método de extensão na maioria dos tipos que podem ser representados em uma lista de parâmetros do Visual Basic, incluindo o seguinte:

  • Classes (tipos de referência)
  • Estruturas (tipos de valor)
  • Interfaces
  • Delegados
  • Argumentos ByRef e ByVal
  • Parâmetros de método genérico
  • Matrizes

Como o primeiro parâmetro especifica o tipo de dados que o método de extensão estende, ele é necessário e não pode ser opcional. Por esse motivo, parâmetros Optional e parâmetros ParamArray não podem ser o primeiro parâmetro na lista de parâmetros.

Os métodos de extensão não são considerados na associação tardia. No exemplo a seguir, a instrução anObject.PrintMe() gera uma exceção MissingMemberException, a mesma exceção que você veria se a segunda definição do método de extensão PrintMe fosse excluída.

Option Strict Off
Imports System.Runtime.CompilerServices

Module Module4

    Sub Main()
        Dim aString As String = "Initial value for aString"
        aString.PrintMe()

        Dim anObject As Object = "Initial value for anObject"
        ' The following statement causes a run-time error when Option
        ' Strict is off, and a compiler error when Option Strict is on.
        'anObject.PrintMe()
    End Sub

    <Extension()> 
    Public Sub PrintMe(ByVal str As String)
        Console.WriteLine(str)
    End Sub

    <Extension()> 
    Public Sub PrintMe(ByVal obj As Object)
        Console.WriteLine(obj)
    End Sub

End Module

Práticas recomendadas

Os métodos de extensão fornecem uma forma conveniente e poderosa de estender um tipo existente. No entanto, para usá-los com êxito, há alguns pontos a serem considerados. Essas considerações se aplicam principalmente a autores de bibliotecas de classes, mas podem afetar qualquer aplicativo que use métodos de extensão.

Geralmente, os métodos de extensão que você adiciona aos tipos que você não tem são mais vulneráveis do que os métodos de extensão adicionados aos tipos que você controla. Várias coisas podem ocorrer em classes que você não tem que podem interferir com seus métodos de extensão.

  • Se houver um membro de instância acessível que tenha uma assinatura compatível com os argumentos na instrução de chamada, sem conversões de restrição necessárias de argumento para parâmetro, o método de instância será usado em preferência para qualquer método de extensão. Portanto, se um método de instância apropriado for adicionado a uma classe em algum momento, um membro de extensão existente no qual você depende poderá se tornar inacessível.

  • O autor de um método de extensão não pode impedir que outros programadores gravem métodos de extensão conflitantes que possam ter precedência sobre a extensão original.

  • Você pode melhorar a robustez colocando métodos de extensão no próprio namespace deles. Os consumidores da biblioteca podem incluir um namespace ou excluí-lo ou selecionar entre namespaces, separadamente do restante da biblioteca.

  • Pode ser mais seguro estender interfaces do que estender classes, especialmente se você não tem a interface ou a classe. Uma alteração em uma interface afeta todas as classes que a implementam. Portanto, o autor pode ser menos propenso a adicionar ou alterar métodos em uma interface. No entanto, se uma classe implementar duas interfaces que têm métodos de extensão com a mesma assinatura, nenhum método de extensão ficará visível.

  • Estenda o tipo mais específico que puder. Em uma hierarquia de tipos, se você selecionar um tipo do qual muitos outros tipos são derivados, há camadas de possibilidades para a introdução de métodos de instância ou outros métodos de extensão que podem interferir no seu.

Métodos de extensão, métodos de instância e propriedades

Quando um método de instância no escopo tem uma assinatura compatível com os argumentos de uma instrução de chamada, o método de instância é escolhido em preferência para qualquer método de extensão. O método de instância tem precedência mesmo que o método de extensão seja uma correspondência melhor. No exemplo a seguir, ExampleClass contém um método de instância chamado ExampleMethod que tem um parâmetro de tipo Integer. O método de extensão ExampleMethod estende ExampleClass e tem um parâmetro de tipo Long.

Class ExampleClass
    ' Define an instance method named ExampleMethod.
    Public Sub ExampleMethod(ByVal m As Integer)
        Console.WriteLine("Instance method")
    End Sub
End Class

<Extension()> 
Sub ExampleMethod(ByVal ec As ExampleClass, 
                  ByVal n As Long)
    Console.WriteLine("Extension method")
End Sub

A primeira chamada para o código a ExampleMethod seguir chama o método de extensão, pois arg1 é Long e é compatível apenas com o parâmetro Long no método de extensão. A segunda chamada para ExampleMethod tem um argumento Integer, arg2, e chama o método de instância.

Sub Main()
    Dim example As New ExampleClass
    Dim arg1 As Long = 10
    Dim arg2 As Integer = 5

    ' The following statement calls the extension method.
    example.exampleMethod(arg1)
    ' The following statement calls the instance method.
    example.exampleMethod(arg2)
End Sub

Agora, inverta os tipos de dados dos parâmetros nos dois métodos:

Class ExampleClass
    ' Define an instance method named ExampleMethod.
    Public Sub ExampleMethod(ByVal m As Long)
        Console.WriteLine("Instance method")
    End Sub
End Class

<Extension()> 
Sub ExampleMethod(ByVal ec As ExampleClass, 
                  ByVal n As Integer)
    Console.WriteLine("Extension method")
End Sub

Desta vez, o código em Main chama o método de instância ambas as vezes. Isso ocorre porque ambos arg1 e arg2 têm uma conversão de expansão para Long e o método de instância tem precedência sobre o método de extensão em ambos os casos.

Sub Main()
    Dim example As New ExampleClass
    Dim arg1 As Long = 10
    Dim arg2 As Integer = 5

    ' The following statement calls the instance method.
    example.ExampleMethod(arg1)
    ' The following statement calls the instance method.
    example.ExampleMethod(arg2)
End Sub

Portanto, um método de extensão não pode substituir um método de instância existente. No entanto, quando um método de extensão tem o mesmo nome de um método de instância, mas as assinaturas não entram em conflito, ambos os métodos podem ser acessados. Por exemplo, se a classe ExampleClass contiver um método nomeado ExampleMethod que não usa argumentos, os métodos de extensão com o mesmo nome, mas assinaturas diferentes são permitidos, conforme mostrado no código a seguir.

Imports System.Runtime.CompilerServices

Module Module3

    Sub Main()
        Dim ex As New ExampleClass
        ' The following statement calls the extension method.
        ex.ExampleMethod("Extension method")
        ' The following statement calls the instance method.
        ex.ExampleMethod()
    End Sub

    Class ExampleClass
        ' Define an instance method named ExampleMethod.
        Public Sub ExampleMethod()
            Console.WriteLine("Instance method")
        End Sub
    End Class

    <Extension()> 
    Sub ExampleMethod(ByVal ec As ExampleClass, 
                  ByVal stringParameter As String)
        Console.WriteLine(stringParameter)
    End Sub

End Module

A saída desse código é a seguinte:

Extension method
Instance method

A situação é mais simples com propriedades: se um método de extensão tiver o mesmo nome que uma propriedade da classe que ele estende, o método de extensão não estará visível e não poderá ser acessado.

Precedência do método de extensão

Quando dois métodos de extensão que têm assinaturas idênticas estiverem no escopo e acessíveis, aquele com precedência mais alta será invocado. A precedência de um método de extensão baseia-se no mecanismo usado para colocar o método no escopo. A lista a seguir mostra a precedência da hierarquia, da mais alta para a mais baixa.

  1. Métodos de extensão definidos dentro do módulo atual.

  2. Métodos de extensão definidos dentro de tipos de dados no namespace atual ou em qualquer um dos pais dele, com namespaces filho com precedência maior do que namespaces pai.

  3. Métodos de extensão definidos dentro de qualquer tipo de importação no arquivo atual.

  4. Métodos de extensão definidos dentro de qualquer namespace no arquivo atual.

  5. Métodos de extensão definidos dentro de qualquer importação de tipo no nível do projeto.

  6. Métodos de extensão definidos dentro de qualquer importação de namespace do projeto.

Se a precedência não resolver a ambiguidade, você poderá usar o nome totalmente qualificado para especificar o método que você está chamando. Se o método Print no exemplo anterior for definido em um módulo nomeado StringExtensions, o nome totalmente qualificado será StringExtensions.Print(example) em vez de example.Print().

Confira também