Métodos de extensão – o que preciso, como quero

O que preciso, como quero… mais ou menos. Mais para mais. Comecemos por o básico: o que são métodos de extensão? Em poucas palavras, métodos de extensão são uma forma de injetar funcionalidades escritas por “nós”, personalizadas, diretamente em tipos que tomamos como “fechados”, quer sejam os escritos por a Microsoft ou os escritos por o vizinho de cima. Quando escrevo “injetar funcionalidades” estou-me a referir a métodos implementados por nós que para o Visual Studio fazem parte de determinada classe, e que podem ser chamados a partir de uma instância.

Isto significa que podemos até acrescentar métodos diretamente na classe String ou Integer? Bom, sim!

Note-se que os métodos de extensão não se tratam de material novo! Foram implementados na versão 9 do VB (incluída no Visual Studio 2008). São no entanto, (e por razões que desconheço) pouco conhecidos, se bem que tenha a certeza de que já tenham usado extensões centenas de vezes sem darem por isso. Basta continuar a ler, não só para saber onde tem usado extensões e como é que as pode identificar, mas também para aprender (naturalmente) como as escrever. É fácil. Prometo.

A esta altura é pertinente, com certeza: “como?”

Como devem calcular, não estamos na verdade a escrever o método diretamente na classe alvo. O método é descrito e implementado em um módulo e tem de estar devidamente assinalado como extensão. Por exemplo:

<Extension()>
Public Sub FazerNada(Str As String)
    'faço o que prometo!
End Sub

É um método de extensão da classe String.

A classe Extension fornecida como atributo do método pertence à classe CompilerServices em System.Runtime.CompilerServices. O módulo que usarem para descrever as extensões deverá importar este namespace para facilitar o uso do atributo.

O tipo que o método pretende estender é definido por o tipo do primeiro parâmetro, o que obriga a que um método de extensão tenha, no mínimo, um parâmetro. Se existirem mais parâmetros, já irão fazer parte da assinatura do método. Estamos a falar, portanto, de magia do compilador! O Visual Basic fica a saber que tem de invocar o método como sendo um método auxiliar comum, mas leva automaticamente a referência à instância que originou a invocação no primeiro parâmetro.

Eu tenho o meu módulo de métodos auxiliares. Vou usar extensões para quê?

Os métodos de extensão são, no fundo, uma encarnação dos vossos métodos auxiliares… mas no devido contexto. Só para estarmos a pensar na mesma situação, quando escrevo métodos auxiliares estou a pensar naqueles pequenos métodos que ajudam a desempenhar um ou vários processamentos sobre um tipo, como por exemplo, validar uma String, formatar uma String, verificar se um número é ímpar, remover duplicados de um array, contar o número de palavras de uma String, e por aí adiante. Vou utilizar, a título de exemplo, um método auxiliar que coloca um ponto final em uma frase (String) e uma letra “grande” no início, por exemplo: "gostava de escrever melhor" passa para "Gostava de escrever melhor."

Public Sub Pontuar(ByRef Frase As String)
    Frase =
        Frase(0).ToString.ToUpperInvariant &
        Frase.Substring(1)
    For Each final As String In {".", "!", "?"}
        If Frase.EndsWith(final) Then Exit Sub
    Next
    Frase &= "."
End Sub

Traduzindo este método auxiliar para extensão, ficaria:

<Extension()>
Public Sub Pontuar(ByRef Frase As String)
    Frase =
        Frase(0).ToString.ToUpperInvariant &
        Frase.Substring(1)
    For Each final As String In {".", "!", "?"}
        If Frase.EndsWith(final) Then Exit Sub
    Next
    Frase &= "."
End Sub

Não precisam de procurar diferenças. Eu digo-vos o que mudou: nada. A grande maioria dos métodos auxiliares podem ser traduzidos apenas colocando o atributo que o transforma em extensão. Neste caso, a única diferença nem sequer está relacionada com a implementação em si, trata-se apenas do atributo do método.
Onde é que está então a grande diferença? Existem duas grandes diferenças, mas não estão na forma como se descreve ou implementa o método. Estão na forma como o método é invocado e descoberto.

IntellisenseA primeira grande vantagem de utilizar extensões, em detrimento de métodos auxiliares, é o facto de estas se encontrarem perfeitamente integradas com o Intellisense (as caixas de sugestão de contexto), no contexto do tipo que estão a estender.

Extensão vs métodoSendo frase do tipo String, as sugestões dos membros vão incluir no separador All as nossas extensões desse tipo. Se não fosse extensão, o método teria de ser chamado de forma arbitrária, ou seja, eu teria de me lembrar que existia um método chamado Pontuar.

A segunda grande diferença/vantagem passa também por aqui: não é necessário fornecer a referência a trabalhar, explicitamente. O Visual Basic sabe que a referência a trabalhar é a mesma que provocou a invocação. Isto possibilita invocações em cadeia. Neste caso não é evidente, porque uma segunda chamada ao método Pontuar na mesma referência não vai alterar nada, e também não é possível, dado que o método Pontuar não produz valor, mas a título de exemplo vamos assumir que o método Pontuar é uma Function que devolve uma String que representa a frase pontuada.
Na abordagem sem extensões, aplicar o método duas vezes seria da seguinte forma:

frase = Pontuar(Pontuar(frase))

O resultado da invocação interior era utilizado como alvo de trabalho na invocação exterior. Com extensão, a sintaxe torna-se muito mais natural, e está acompanhada por o Intellisense:

frase = frase.Pontuar.Pontuar()

frase é uma String que é processada com a primeira extensão e produz uma String que é processada com a extensão que lhe sucede.

Existe uma inegável vantagem em utilizar extensões em tudo o que toca, não só à organização e legibilidade do código, mas também ao vosso índice de produtividade. Enfim, todas as vantagens que se podem ter com a utilização do Intellisense, aplicadas a um pedaço de código escrito para trabalhar tipos que não controlamos diretamente. E quem diz métodos auxiliares diz qualquer tipo de método. Manda a necessidade e criatividade.

Alguns exemplos de extensões

É possível estender classes, estruturas, interfaces e delegates.
Para demonstrar vários tipos de assinaturas e extensões, deixo-vos com alguns exemplos. De notar que são sugestões de implementação, não garanto que sejam as formas mais eficazes de desempenhar a função.

Posso começar por reescrever a extensão Pontuar para que produza um valor, que é como deve ser implementada:

<Extension()>
Public Function Pontuar(ByRef Frase As String) As String
    Frase =
        Frase(0).ToString.ToUpperInvariant &
        Frase.Substring(1)
    For Each final As String In {".", "!", "?"}
        If Frase.EndsWith(final) Then Return Frase
    Next
    Return Frase & "."
End Function

Invocação: MinhaFrase.Pontuar()

O único parâmetro indica a classe que vai estender, a String. Sem parâmetros adicionais.

Um outro exemplo de extensão, desta vez uma extensão do Integer que indica se determinada instância representa um número ímpar ou não:

<Extension()>
Public Function NumeroImpar(ByVal num As Integer) As Boolean
    If num Mod 2 = 0 Then Return False Else Return True
End Function

Invocação: MeuNumero.NumeroImpar()

As extensões também não precisam forçosamente de trabalhar a instância. Podem simplesmente fazer qualquer outra coisa com ela. Segue um exemplo que estende qualquer classe e que escreve para debug a sua representação em String:

<Extension()>
Public Sub [Debug](valor As Object)
    Dim momento As DateTime = Now()
    Dim output As New System.Text.StringBuilder("[")
    output.Append(momento.ToShortDateString)
    output.Append("][")
    output.Append(momento.ToShortTimeString)
    output.Append("] Dump de variável: ")
    output.Append(valor.ToString)
    System.Diagnostics.Debug.WriteLine(output.ToString)
End Sub

Invocação: MinhaVariavel.Debug()

Uso os parêntesis retos porque Debug já é o nome de uma classe existente nos namespaces importados por defeito. Ainda que neste caso não houvesse perigo de colisão, é boa prática.  Com uma simples invocação, o método regista em debug a data e hora, seguida da representação.

Posso dar um outro exemplo de uma extensão que não transforma a instância, mas faz algo diferente com ela:

<Extension()>
Public Sub Falar(texto As String)
    Dim s As New System.Speech.Synthesis.SpeechSynthesizer()
    s.Speak(texto)
End Sub

Invocação: TextoAFalar.Falar()

Estendemos a String e usamos o sintetizador de discurso para “falar” o conteúdo da String.

As extensões também são úteis quando queremos saber outro tipo de informações relacionadas com o tipo da instância, como por exemplo, devolver o número de palavras numa String:

<Extension()>
Public Function Palavras(ByVal texto As String) As Integer
    While texto.Contains(" ") Or texto.Contains(vbTab)
        texto = texto.Replace(" ", " ").texto.Replace(vbTab, " ")
    End While
    Dim p As String() = texto.Split(" ")
    Dim inc As Integer = 0
    For Each tmp As String In p
        If tmp.Length > 1 Then inc += 1
    Next
    Return inc
End Function

Invocação: AMinhaFrase.Palavras()

Por fim, e como exemplo mais interessante, podemos jogar com os tipos genéricos e implementar um método semelhante ao Where das coleções de IEnumerable, mas para aplicar em arrays:

<Extension()>
Public Function Filtrar(Of T)(arr As T(), filtro As Func(Of T, Boolean)) As Array
    Dim ret(0) As T
    For i As Integer = 0 To arr.Length - 1
        Dim tmp As T = arr(i)
        If filtro.Invoke(tmp) Then
            If i < arr.Length - 1 And i <> 0 Then ReDim Preserve ret(ret.Length)
            ret(ret.Length - 1) = tmp
        End If
    Next
    Return ret
End Function

Ao indicarmos que o primeiro parâmetro é do tipo Array de T, estamos na prática a indicar que estamos a estender qualquer array de qualquer tipo. Como existem especificidades para comparar cada tipo, passamos um segundo parâmetro (que é na verdade o único, como já sabem) que é um delegate. A invocação deste método tem de levar como parâmetro uma função que devolva True ou False, sendo o True para determinar elementos que obedecem ao filtro, e False os que se vão descartar.

Invocação exemplo: MinhaArrayDeInteger.Filtrar(Function(el) el < 10)

Esta invocação resulta num novo array cujos elementos são os da instância invocadora, quando estes inferiores a 10. Ou seja, por exemplo:

{2,45,67,4,9,10}.Filtrar(Function(el) el < 10)

Como identificar extensões que já usava?

Identificar extensãoJá tiveram, certamente, oportunidade de reparar que a extensão é identificada na caixa de sugestões com um símbolo ligeiramente diferente do símbolo do método: a extensão apresenta uma pequena seta escura, no sentido descendente, do lado direito.

Quantas vezes já utilizaram métodos Microsoft com este símbolo? As extensões mais famosas, e que mais devem usar, são as extensões LINQ. Repararam que o Where, o Find, e até o Count de coleções de IEnumerables são extensões?

Publicado na edição 45 (PDF) da Revista PROGRAMAR.