Métodos de extensión (Visual Basic)

Los métodos de extensión permiten a los desarrolladores agregar funcionalidad personalizada a los tipos de datos que ya están definidos sin necesidad de crear un nuevo tipo derivado. Los métodos de extensión permiten escribir un método al que se puede llamar como si fuera un método de instancia del tipo existente.

Comentarios

Un método de extensión solo puede ser un procedimiento Sub o un procedimiento Function. No se puede definir una propiedad, un campo o un evento de extensión. Todos los métodos de extensión deben marcarse con el atributo de extensión <Extension> del espacio de nombres System.Runtime.CompilerServices y deben definirse en un módulo. Si un método de extensión se define fuera de un módulo, el compilador de Visual Basic genera el error BC36551, "Los métodos de extensión solo se pueden definir en módulos".

El primer parámetro de una definición de método de extensión especifica qué tipo de datos extiende el método. Cuando se ejecuta el método, el primer parámetro se enlaza a la instancia del tipo de datos que invoca al método.

El atributo Extension solo se puede aplicar a un elemento Module, Sub o Function de Visual Basic. Si se aplica a Class o Structure, el compilador de Visual Basic genera el error BC36550, "El atributo 'Extension' solo se puede aplicar a declaraciones 'Module', 'Sub' o 'Function'".

Ejemplo

En el ejemplo siguiente se define una extensión Print para el tipo de datos String. El método usa Console.WriteLine para mostrar una cadena. El parámetro del método Print, aString, establece que el método extiende la clase String.

Imports System.Runtime.CompilerServices

Module StringExtensions

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

End Module

Observe que la definición del método de extensión está marcada con el atributo de extensión <Extension()>. Marcar el módulo en el que se define el método es opcional, aunque debe marcarse cada método de extensión. System.Runtime.CompilerServices debe importarse para acceder al atributo de extensión.

Los métodos de extensión solo se pueden declarar en módulos. Normalmente, el módulo en el que se define un método de extensión no es el mismo módulo en el que se llama. En su lugar, el módulo que contiene el método de extensión se importa, si fuera necesario, para incluirlo en el ámbito. Una vez que el módulo que contiene Print está en el ámbito, se puede llamar al método como si fuera un método de instancia normal que no toma ningún argumento, 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

El ejemplo siguiente, PrintAndPunctuate, también es una extensión de String, esta vez definido con dos parámetros. El primer parámetro, aString, establece que el método de extensión extiende String. El segundo parámetro, punc, está pensado para ser una cadena de signos de puntuación que se pasa como argumento cuando se llama al método. El método muestra la cadena seguida de las marcas de puntuación.

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

Se llama al método mediante el envío de un argumento de cadena para punc: example.PrintAndPunctuate(".")

En el ejemplo siguiente se muestran Print y PrintAndPunctuate definidos y llamados. System.Runtime.CompilerServices se importa en el módulo de definición para habilitar el acceso al atributo de extensión.

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

A continuación, los métodos de extensión se incluyen en el ámbito y se llaman:

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

Todo lo necesario para poder ejecutar estos métodos de extensión o similares es que estén en el ámbito. Si el módulo que contiene un método de extensión está en el ámbito, es visible en IntelliSense y se puede llamar como si fuera un método de instancia normal.

Observe que cuando se invocan los métodos, no se envía ningún argumento para el primer parámetro. El parámetro aString de las definiciones de método anteriores está enlazado a example, la instancia de String que las llama. El compilador usa example como argumento enviado al primer parámetro.

Si se llama a un método de extensión de un objeto que está establecido en Nothing, el método de extensión se ejecuta. Esto no se aplica a los métodos de instancia normales. Puede comprobar Nothing explícitamente en el método de extensión.

Tipos que se pueden extender

Puede definir un método de extensión en la mayoría de los tipos que se pueden representar en una lista de parámetros de Visual Basic, incluidos los siguientes:

  • Clases (tipos de referencia)
  • Estructuras (tipos de valor)
  • Interfaces
  • Delegados
  • Argumentos ByRef y ByVal
  • Parámetros de método genéricos
  • Matrices

Dado que el primer parámetro especifica el tipo de datos que extiende el método de extensión, es necesario y no puede ser opcional. Por ese motivo, los parámetros Optional y los parámetros ParamArray no pueden ser el primer parámetro de la lista de parámetros.

Los métodos de extensión no se tienen en cuenta en el enlace tardío. En el ejemplo siguiente, la instrucción anObject.PrintMe() genera una excepción MissingMemberException, la misma excepción que se vería si se eliminara la segunda definición del método de extensión PrintMe.

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

Procedimientos recomendados

Los métodos de extensión proporcionan una manera cómoda y eficaz de extender un tipo existente. Pero para usarlos correctamente, hay algunos puntos que se deben tener en cuenta. Estas consideraciones se aplican principalmente a los autores de bibliotecas de clases, pero pueden afectar a cualquier aplicación que use métodos de extensión.

Por lo general, los métodos de extensión que se agregan a tipos que no se poseen son más vulnerables que los métodos de extensión agregados a tipos que se controlan. Pueden ocurrir varias cosas en las clases que no se poseen que pueden interferir con los métodos de extensión.

  • Si existe algún miembro de instancia accesible que tenga una firma compatible con los argumentos de la instrucción de llamada, sin conversiones de restricción necesarias del argumento al parámetro, se prefiere usar el método de instancia a cualquier método de extensión. Por lo tanto, si se agrega un método de instancia adecuado a una clase en algún momento, un miembro de extensión existente en el que se base puede volverse inaccesible.

  • El autor de un método de extensión no puede evitar que otros programadores escriban métodos de extensión en conflicto que puedan tener prioridad sobre la extensión original.

  • Puede mejorar la solidez si coloca métodos de extensión en su propio espacio de nombres. Luego, los consumidores de la biblioteca pueden incluir un espacio de nombres o excluirlo, o seleccionar entre espacios de nombres, independientemente del resto de la biblioteca.

  • Puede ser más seguro extender interfaces que extender clases, especialmente si no se posee la interfaz o clase. Un cambio en una interfaz afecta a cada clase que la implementa. Por lo tanto, es menos probable que el autor agregue o cambie métodos en una interfaz. Pero si una clase implementa dos interfaces que tienen métodos de extensión con la misma firma, ninguno de los métodos de extensión es visible.

  • Extienda el tipo más específico que pueda. En una jerarquía de tipos, si selecciona un tipo del que se derivan muchos otros tipos, hay capas de posibilidades para la incorporación de métodos de instancia u otros métodos de extensión que podrían interferir con el suyo.

Métodos de extensión, métodos de instancia y propiedades

Si un método de instancia del ámbito tiene una firma que es compatible con los argumentos de una instrucción de llamada, se prefiere el método de instancia a cualquier método de extensión. El método de instancia tiene prioridad aunque el método de extensión sea una mejor coincidencia. En el ejemplo siguiente, ExampleClass contiene un método de instancia de nombre ExampleMethod que tiene un parámetro de tipo Integer. El método de extensión ExampleMethod extiende ExampleClass y tiene un 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

La primera llamada a ExampleMethod en el código siguiente llama al método de extensión, porque arg1 es Long y solo es compatible con el parámetro Long del método de extensión. La segunda llamada a ExampleMethod tiene un argumento Integer, arg2, y llama al método de instancia.

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

Ahora invierta los tipos de datos de los parámetros de los dos 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

Esta vez, el código de Main llama al método de instancia ambas veces. Esto se debe a que tanto arg1 como arg2 tienen una conversión de ampliación a Long, y el método de instancia tiene prioridad sobre el método de extensión en ambos 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

Por lo tanto, un método de extensión no puede reemplazar a un método de instancia existente. Pero si un método de extensión tiene el mismo nombre que un método de instancia, pero las firmas no entran en conflicto, se puede acceder a ambos métodos. Por ejemplo, si la clase ExampleClass contiene un método de nombre ExampleMethod que no toma argumentos, se permiten métodos de extensión con el mismo nombre pero firmas diferentes, como se muestra en el código siguiente.

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

El resultado de este código es el siguiente:

Extension method
Instance method

La situación es más sencilla con propiedades: si un método de extensión tiene el mismo nombre que una propiedad de la clase que extiende, el método de extensión no es visible y no se puede acceder a él.

Prioridad del método de extensión

Si dos métodos de extensión que tienen firmas idénticas están en el ámbito y son accesibles, se invoca al de mayor prioridad. La prioridad de un método de extensión se basa en el mecanismo empleado para incluirlo en el ámbito. En la siguiente lista se muestra la jerarquía de prioridad, de mayor a menor.

  1. Métodos de extensión definidos dentro del módulo actual.

  2. Métodos de extensión definidos dentro de tipos de datos del espacio de nombres actual o cualquiera de sus elementos primarios, donde los espacios de nombres secundarios tienen mayor prioridad que los espacios de nombres primarios.

  3. Métodos de extensión definidos dentro de cualquier tipo de importación del archivo actual.

  4. Métodos de extensión definidos dentro de cualquier importación de espacio de nombres del archivo actual.

  5. Métodos de extensión definidos dentro de cualquier importación de tipo de nivel de proyecto.

  6. Métodos de extensión definidos dentro de cualquier importación de espacio de nombres de nivel de proyecto.

Si la prioridad no resuelve la ambigüedad, puede usar el nombre completo para especificar el método al que llama. Si el método Print del ejemplo anterior se define en un módulo de nombre StringExtensions, el nombre completo es StringExtensions.Print(example) en lugar de example.Print().

Consulte también