Instintos básicos

Co y contravarianza genérica de Visual Basic 2010

Binyam Kelile

Visual Studio 2010 tiene una nueva característica denominada co y contravarianza genérica que se encuentra disponible al trabajar con interfaces y delegados genéricos. En versiones anteriores a Visual Studio 2010 y Microsoft .NET Framework 4, los genéricos se comportan sin variaciones con respecto a los subtipos, de modo que no se permiten conversiones entre tipos genéricos con argumentos de tipo diferente.

Por ejemplo, si intenta transmitir una List(Of Derived) a un método que acepta un IEnumerable(Of Base), se generará un error. Sin embargo, Visual Studio 2010 puede controlar co y contravarianza de seguridad de tipos que admite la declaración de parámetros de tipo covariante y contravariante en tipos de interfaces y delegados genéricos. En este artículo, analizaré en qué consiste realmente esta característica y cómo puede aprovecharla en sus aplicaciones.

Puesto que un botón es un control, se espera que este código funcione debido a los principios básicos de herencia orientados a objetos:

Dim btnCollection As IEnumerable(Of Button) = New List(Of Button) From {New Button}
Dim ctrlCollection As IEnumerable(Of Control) = btnCollection

Sin embargo, no está permitido en Visual Studio 2008, lo que generará el mensaje de error “IEnumerable(Of Button) cannot be converted to IEnumerable(Of Control)”. Pero como programadores orientados a objetos, sabemos que un valor de tipo Button se puede convertir en un control; por lo tanto, como se indicó anteriormente, de acuerdo con los principios básicos de herencia, se debe permitir el código.

Veamos el ejemplo siguiente:

Dim btnCollection As IList(Of Button) = New List(Of Button) From {New Button}
Dim ctrlCollection As IList(Of Control) = btnCollection
ctrlCollection(0) = New Label
Dim firstButton As Button = btnCollection(0)

Esto generaría un error con InvalidCastException porque el programador convirtió IList(Of Button) en IList(Of Control) y después insertó un control en éste que ni siquiera era un botón.

Visual Studio 2010 reconoce y permite código como éste en el primer caso, pero aún puede no permitir el tipo de código que se muestra en el segundo caso cuando está orientado a .NET Framework 4. Para la mayor parte de los usuarios, y casi todo el tiempo, los programas sólo funcionarán en la forma programada y no será necesario hacer un análisis en profundidad. Sin embargo, en este artículo, haré un análisis en profundidad para explicar cómo y por qué funciona el código.

Covarianza

En la primera lista, que visualizó un IEnumerable(Of Button) como un IEnumerable(Of Control), ¿por qué el código era seguro en Visual Studio 2010, mientras que el segundo ejemplo de código, que visualizó un IList(Of Button) como un IList(Of Control), era inseguro?

El primero es seguro porque IEnumerable(Of T) es una interfaz de “salida”, lo que significa que en IEnumerable(Of Control), los usuarios de la interfaz pueden sacar controles de la lista.

El segundo es inseguro porque IList(Of T) es una interfaz de “entrada y salida”, de modo que en IList(Of Control), los usuarios de la interfaz pueden ingresar controles y también pueden sacarlos.

La nueva característica de lenguaje en Visual Studio 2010 que permite esto se denomina covarianza genérica. En .NET Framework 4, Microsoft ha vuelto a escribir el marco junto con estas líneas:

Interface IEnumerable(Of Out T)
...
End Interface
Interface IList(Of T)
...
End Interface

La anotación Out en IEnumerable(Of Out T) indica que si un método en IEnumerable menciona T, sólo lo hará en una posición de salida, tal como esa para la devolución de una función o el tipo de una propiedad de sólo lectura. Esto permite que los usuarios conviertan cualquier IEnumerable(Of Derived) en un IEnumerable(Of Base) sin ejecutarlo en un InvalidCastException.

A IList le falta una anotación porque IList(Of T) es una interfaz de entrada y salida. Como resultado, los usuarios no puede convertir IList(Of Derived) en IList(Of Base) o viceversa; hacer eso podría ocasionar InvalidCastException, como lo observó anteriormente.

Contravarianza

Existe un espejo de la anotación Out. Es un poco más sutil, de modo que empezaré con un ejemplo:

Dim _compOne As IComparer(Of Control) = New MyComparerByControlName()
Dim _compTwo As IComparer(Of Button) = _compOne
Dim btnOne = new Button with {.Name = "btnOne", OnClick = AddressOf btnOneClick, Left=20}
Dim btnTwo = new Button with {.Name = "btnTwo", OnClick = AddressOf btnTwoClick, Left=100}
Dim areSame = _compTwo.Compare(btnOne, btnTwo)

Aquí he creado un comparador que puede determinar si cualquiera de los dos controles son el mismo, lo que hace mirando sólo sus nombres.

Puesto que el comparador puede comparar cualquier control, indudablemente tiene la capacidad de evaluar dos controles que resultan ser botones. Ese es el motivo por el cual puede convertirlo en IComparer(Of Button). En general, puede convertir de forma segura un IComparer(Of Base) en cualquier IComparer(Of Derived). Esto se denomina contravarianza. Se realiza con anotaciones In y es la imagen de espejo idéntica de las anotaciones Out.

.NET Framework 4 también se ha modificado para incorporar parámetros de tipo genérico In:

Interface IComparer(Of In T)
 ...
End Interface

Debido a la anotación In de IComparer(Of T), cada método en IComparer que menciona T sólo lo hará en una posición de entrada, por ejemplo, de un argumento ByVal o del tipo de una propiedad de sólo escritura. De ese modo, los usuarios pueden convertir un IComparer(Of Base) en cualquier IComparer(Of Derived) sin ejecutarlo en un InvalidCastException.

Observemos un ejemplo del delegado Action en .NET 4, en que el delegado se transforma en contravariante en T:

Dim actionControl As Action(Of Control)
Dim actionButton As Action(Of Button) = actionControl

El ejemplo funciona en.NET 4 porque el usuario del delegado actionButton siempre lo invocará con argumentos Button, que son controles.

También puede agregar anotaciones In y Out a sus propias interfaces y delegados genéricos. Sin embargo, debido a las limitaciones de Common Language Runtime (CLR), puede usar estas anotaciones en clases, estructuras o en cualquier otra cosa. En resumen, sólo las interfaces y los delegados pueden ser covariantes o contravariantes.

Declaraciones/sintaxis

Visual Basic usa dos palabras clave contextuales nuevas: Out, que presenta covarianza e In, que hace lo mismo para la contravarianza, como se muestra en este ejemplo:

Public Delegate Function Func(Of In TArg, Out TResult)(ByVal arg As TArg) As TResult

Public Interface IEnumerable(Of Out Tout)

  Inherits IEnumerable
  Function GetEnumerator() As IEnumerator(Of Tout)
End Interface

Public Interface IEnumerator(Of Out Tout)
  Inherits IEnumerator
  Function Current() As Tout
End Interface

Public Interface IComparer(Of In Tin)
  Function Compare(ByVal left As Tin, ByVal right As Tin) As Integer
End Interface

Sin embargo, ¿por qué necesitamos estas dos palabras clave contextuales o la sintaxis? ¿Por qué no inferimos la varianza In/Out automáticamente? En primer lugar, es útil que los programadores declaren su intención. En segundo lugar, existen lugares en que el compilador puede inferir automáticamente la mejor anotación de varianza.

Analicemos dos interfaces, IReadWriteBase e IReadWrite:

Interface IReadWriteBase(Of U)
  Function ReadWrite() As IReadWrite(Of U)
End Interface
Interface IReadWrite(Of T) : Inherits IReadWriteBase(Of T)
End Interface

Si el compilador infiere que ambas son Out, como a continuación, el código funciona bien:

Interface IReadWriteBase(Of Out U)
  Function ReadWrite() As IReadWrite(Of U)
End Interface
Interface IReadWrite(Of Out T)
  Inherits IReadWriteBase(Of T)
End Interface

Y si el compilador infiere que ambas son In, como se muestra aquí, también el código funciona bien:

Interface IReadWrite(Of In T)
  Inherits IReadWriteBase(Of T)
End Interface
Interface IReadWriteBase(Of In U)
  Function ReadWrite() As IReadWrite(Of U)
End Interface

El compilador no puede saber cuál seleccionar, In u Out; por lo tanto, proporciona una sintaxis.

Las palabras clave contextuales Out/In aparecen sólo en declaraciones de interfaz y de delegado. Usar las palabras clave en cualquier otra declaración de parámetro genérico generará un error en tiempo de compilación. El compilador de Visual Basic no permite que una interfaz de varianza contenga enumeraciones, clases y estructuras anidadas, porque CLR no admite clases de varianza. Sin embargo, puede anidar interfaces de varianza dentro de una clase.

Cómo administrar la ambigüedad

Las co y contravarianza presentan ambigüedad en la búsqueda de miembros, de modo que debe saber qué activa la ambigüedad y cómo el compilador de Visual Basic la controla.

Analicemos el ejemplo de la figura 1, en que intentamos convertir Comparer en IComparer(Of Control), donde Comparer implementa IComparer(Of Button) e IComparer(Of CheckBox).

Figura 1 Conversión ambigua

Option Strict On
Imports System.Windows.Forms
Interface IComparer(Of Out Tout) 
End Interface
Class Comparer 
    Implements IComparer(Of Button) 
    Implements IComparer(Of CheckBox) 
End Class

Module VarianceExample
    Sub Main()
        Dim iComp As IComparer(Of Control) = New Comparer()
    End Sub
End Module

Puesto que tanto IComparer(Of Button) como IComparer(Of CheckBox) son varianzas convertibles en IComparer(Of Control), la conversión será ambigua. Como resultado, el compilador de Visual Basic busca ambigüedades conforme a las reglas de CLR, y si Option Strict es On, no permite esas conversiones ambiguas en tiempo de compilación; si Option Strict es Off, el compilador genera una advertencia.

La conversión en la figura 2 se realiza correctamente en tiempo de ejecución y no genera un error en tiempo de compilación.

Figura 2 Conversión que se realiza correctamente en tiempo de ejecución

Option Strict On
Imports System.Windows.Forms
Interface IEnumerable(Of Out Tout) 
End Interface
Class ControlList 
   Implements IEnumerable(Of Button) 
   Implements IEnumerable(Of CheckBox) 
End Class

Module VarianceExample
    Sub Main()
     Dim _ctrlList As IEnumerable(Of Control) = CType(New ControlList, IEnumerable(Of Button))
     Dim _ctrlList2 As IEnumerable(Of Control) = CType(New ControlList, IEnumerable(Of CheckBox))
    End Sub
End Module

En la figura 3 se muestra el peligro de implementar las interfaces genéricas IComparer(of IBase) e IComparer(of IDerived). Aquí, las clases Comparer1 y Comparer2 implementan la misma interfaz genérica de varianza con distintos parámetros de tipo genérico en diferente orden. Aunque las clases Comparer1 y Comparer2 son idénticas, además de ordenar cuando implementan la interfaz, la llamada al método Compare en esas clases proporciona resultados diferentes.

Figura 3 Diferentes resultados con el mismo método

Option Strict Off
Module VarianceExample
    Sub Main()
        Dim _comp As IComparer(Of Account) = New Comparer1()
        Dim _comp2 As IComparer(Of Account) = New Comparer2()

        Dim _account = New Account With {.AccountType = "Checking", .IsActive = True}
        Dim _account2 = New Account With {.AccountType = "Saving", .IsActive = False}

        ‘// Even though _comp and _comp2 are *IDENTICAL*, they give different results!
        Console.WriteLine(_comp.Compare(_account, _account2)) ‘; // prints 0
        Console.WriteLine(_comp2.Compare(_account, _account2)) ‘; // prints -1
    
    End Sub
    Interface IAccountRoot
        Property AccountType As String
    End Interface
    Interface IAccount
        Inherits IAccountRoot
        Property IsActive As Boolean
    End Interface
    Class Account
        Implements IAccountRoot, IAccount
        Public Property AccountType As String Implements IAccountRoot.AccountType
        Public Property IsActive As Boolean Implements IAccount.IsActive
    End Class

    Class Comparer1
        Implements IComparer(Of IAccountRoot), IComparer(Of IAccount)

        Public Function Compare(ByVal x As IAccount, ByVal y As IAccount) As Integer Implements System.Collections.Generic.IComparer(Of IAccount).Compare
            Dim c As Integer = String.Compare(x.AccountType, y.AccountType)
            If (c <> 0) Then
                Return c
            Else
                Return (If(x.IsActive, 0, 1)) - (If(y.IsActive, 0, 1))
            End If

        End Function

        Public Function Compare(ByVal x As IAccountRoot, ByVal y As IAccountRoot) As Integer Implements System.Collections.Generic.IComparer(Of IAccountRoot).Compare
            Return String.Compare(x.AccountType, y.AccountType)
        End Function
    End Class

    Class Comparer2
        Implements IComparer(Of IAccount), IComparer(Of IAccountRoot)

        Public Function Compare(ByVal x As IAccount, ByVal y As IAccount) As Integer Implements System.Collections.Generic.IComparer(Of IAccount).Compare
            Dim c As Integer = String.Compare(x.AccountType, y.AccountType)
            If (c <> 0) Then
                Return c
            Else
                Return (If(x.IsActive, 0, 1)) - (If(y.IsActive, 0, 1))
            End If
        End Function

        Public Function Compare(ByVal x As IAccountRoot, ByVal y As IAccountRoot) As Integer Implements System.Collections.Generic.IComparer(Of IAccountRoot).Compare
            Return String.Compare(x.AccountType, y.AccountType)
        End Function
    End Class
End Module

Este es el motivo por el cual emergen diferentes resultados del código en la figura 3, aunque _comp y _comp2 sean idénticos. El compilador sólo emite el Lenguaje Intermedio de Microsoft que realiza la conversión. De ese modo, la opción de si obtener la interfaz IComparer(Of IAccountRoot) o IComparer(Of IAccount), dada una implementación del método Compare() que es diferente, reside en CLR, que siempre selecciona la primera interfaz compatible con asignación en la lista de interfaces. Por lo tanto, con el código en la figura 3, el método Compare() proporciona diferentes resultados porque CLR elige la interfaz IComparer(Of IAccountRoot) para la clase Comparer1 y la interfaz IComparer(Of IAccount) para la clase Comparer2.

Restricciones en una interfaz genérica

Cuando escribe una restricción genérica As, (Of T As U, U), ahora abarca una capacidad de conversión de varianza además de la herencia. La figura 4 demuestra que (of T As U, U) abarca una capacidad de conversión de varianza.

Figura 4 Cómo una restricción genérica abarca una capacidad de conversión de varianza

Option Strict On
Imports System.Windows.Forms
Module VarianceExample
    Interface IEnumerable(Of Out Tout) 
    End Interface
    Class List(Of T)
        Implements IEnumerable(Of T)
    End Class
    Class Program
        Shared Function Foo(Of T As U, U)(ByVal arg As T) As U
            Return arg
        End Function

        Shared Sub Main()
            ‘This is allowed because it satisfies the constraint Button AS Control
            Dim _ctrl As Control = Foo(Of Button, Control)(New Button)
            Dim _btnList As IEnumerable(Of Button) = New List(Of Button)()
            ‘This is allowed because it satisfies the constraint IEnumerable(Of Button) AS IEnumerable(Of Control)
            Dim _ctrlCol As IEnumerable(Of Control) = Foo(Of IEnumerable(Of Button), IEnumerable(Of Control))(_btnList)

        End Sub
    End Class
End Module

Un parámetro genérico de varianza se puede restringir en un parámetro de varianza diferente. Considere lo siguiente:

Interface IEnumerable(Of In Tin, Out Tout As Tin)
End Interface
Interface IEnumerable(Of Out Tout, In Tin As Tout)
End Interface

En este ejemplo, un IEnumerator(Of ButtonBase, ButtonBase) puede ser una conversión de varianza en un IEnumerator(Of Control, Button) y IEnumerable(Of Control, Button) se puede convertir en IEnumerable(Of ButtonBase, ButtonBase) con todas las restricciones ya satisfechas. Desde un punto de vista nocional, más adelante podría ser una conversión de varianza en IEnumerable(Of ButtonBase, Control), pero esto ya no satisface las restricciones, de modo que no es un tipo válido. La figura 5 representa una colección de objetos primero en entrar, primero en salir, en que una restricción podría ser útil.

Figura 5 Dónde una restricción es útil en una colección de objetos

Option Strict On
Imports System.Windows.Forms

Interface IPipe(Of Out Tout, In Tin As Tout)
    Sub Push(ByVal x As Tin)
    Function Pop() As Tout
End Interface

Class Pipe(Of T)
    Implements IPipe(Of T, T)

    Private m_data As Queue(Of T)

    Public Function Pop() As T Implements IPipe(Of T, T).Pop
        Return m_data.Dequeue()
    End Function

    Public Sub Push(ByVal x As T) Implements IPipe(Of T, T).Push
        m_data.Enqueue(x)
    End Sub
End Class

Module VarianceDemo
    Sub Main()
        Dim _pipe As New Pipe(Of ButtonBase)
        Dim _IPipe As IPipe(Of Control, Button) = _pipe
    End Sub
End Module

En la figura 5, si a usted le proporciono _IPipe, sólo puede insertar Button en la barra vertical y sólo puede leer Control desde ésta. Tenga en cuenta que puede restringir una interfaz de varianza a un tipo de valor, dado que la interfaz nunca permitirá una conversión de varianza. Aquí se muestra un ejemplo con restricción de tipo de valor en un parámetro genérico:

Interface IEnumerable(Of Out Tout As Structure)
End Interface

La restricción a un tipo de valor podría ser inútil, dado que no se permite una conversión de varianza en una interfaz de varianza cuya instancia se creó con tipos de valor. Sin embargo, tenga en cuenta que Tout por el hecho de ser una estructura, puede ser útil inferir el tipo indirectamente mediante restricciones.

Restricciones en los parámetros genéricos de una función

Las restricciones en métodos o funciones deben tener tipos In. A continuación, se muestran dos formas básicas de pensar en las restricciones en parámetros genéricos de una función:

  • En la mayoría de los casos, un parámetro genérico en una función es básicamente una entrada a la función, y todas las entradas deben tener tipos In.
  • Un cliente puede convertir la varianza de cualquier tipo Out en System.Object. Si un parámetro genérico está restringido en algún tipo Out, un cliente efectivamente puede eliminar esa restricción, que no representa en que consisten las restricciones.

Analicemos la figura 6, que deja claro que sucedería sin esta regla de validez de varianza en las restricciones.

Figura 6 Qué sucede sin una regla de validez de varianza en las restricciones

Option Strict On
Imports System.Windows.Forms
Interface IEnumerable(Of Out Tout)
    Sub Foo(Of U As Tout)(ByVal arg As U)
End Interface

Class List(Of T)
    Implements IEnumerable(Of T)

    Private m_data As T
    Public Sub Foo(Of U As T)(ByVal arg As U) Implements IEnumerable(Of T).Foo
        m_data = arg
    End Sub
End Class

Module VarianceExample
    Sub Main()
        ‘Inheritance/Implements
        Dim _btnCollection As IEnumerable(Of Button) = New List(Of Button)
        ‘Covariance
        Dim _ctrlCollection As IEnumerable(Of Control) = _btnCollection
        ‘Ok, Constraint-satisfaction, because Label is a Control
        _ctrlCollection.Foo(Of Label)(New Label)
    End Sub
End Module

En la figura 6, hemos almacenado Label dentro de m_data como Button, lo que es ilegal. Por lo tanto, las restricciones en métodos o funciones deben tener tipos In.

Resolución de sobrecarga

El término sobrecarga se refiere a crear varias funciones con el mismo nombre que toman diferentes tipos de argumento. La resolución de sobrecarga es un mecanismo de tiempo de compilación para seleccionar la mejor función de un conjunto de candidatos.

Analicemos el siguiente ejemplo:

Private Overloads Sub Foo(ByVal arg As Integer)
End Sub
Private Overloads Sub Foo(ByVal arg As String)
End Sub

Foo(2)

¿Qué sucede realmente en segundo plano aquí? Cuando el compilador ve la llamada a Foo(2), debe determinar qué Foo desea invocar. Para hacerlo, use el siguiente algoritmo simple:

  1. Genere un conjunto de todos los candidatos aplicables mediante la búsqueda de todo con el nombre Foo. En nuestro ejemplo, existen dos candidatos que es necesario considerar.
  2. Para cada candidato, observe los argumentos y elimine las funciones no aplicables. Observe que el compilador también realiza una pequeña comprobación y una inferencia de tipos para los genéricos.

Con la presentación de una varianza, un conjunto de conversiones predefinidas se expande, y como resultado, el paso 2 aceptará más funciones candidatas de las que existían antes. Además, en casos en que suela haber dos candidatos igualmente específicos, el compilador seleccionará el candidato no oculto, pero ahora el candidato oculto puede ser más amplio, de modo que el compilador puede seleccionar este último. La figura 7 muestra código que podría verse afectado con la adición de varianza en Visual Basic.

Figura 7 Código que podría verse afectado con la adición de varianza en Visual Basic

Option Strict On
Imports System.Windows.Forms
Imports System.Collections.Generic
Module VarianceExample
    Sub Main()
        Dim _ctrlList = New ControlList(Of Button)
        ‘Picks Add(ByVal f As IEnumerable(Of Control)), Because of variance-convertibility
        _ctrlList.Add(New ControlList(Of Button))
    End Sub
End Module
Interface IEnumerable(Of Tout)
End Interface
Class ControlList(Of T)
    Implements IEnumerable(Of T)

    Sub Add(ByVal arg As Object)
    End Sub

    Sub Add(ByVal arg As IEnumerable(Of Control))
    End Sub
End Class

En Visual Studio 2008, la llamada a Add enlazaría a Object, pero con la conversión de varianza de Visual Studio 2010, en su lugar usamos IEnumerable(Of Control).

El compilador selecciona un candidato de restricción sólo si no existe otro, pero con conversión de varianza; si existe un nuevo candidato de ampliación, el compilador selecciona este último. Si la conversión de varianza hace un nuevo candidato de restricción, el compilador emite un error.

Métodos de extensión

Los métodos de extensión le permiten agregar métodos a los tipos existentes sin crear un nuevo tipo derivado, volviendo a compilar o modificando el tipo original. En Visual Studio 2008, los métodos de extensión admiten covarianza de matriz, como se muestra en el siguiente ejemplo:

Option Strict On
Imports System.Windows.Forms
Imports System.Runtime.CompilerServices
Module VarianceExample
  Sub Main()
    Dim _extAdd(3) As Button ‘Derived from Control
    _extAdd.Add()
  End Sub

  <Extension()>
  Public Sub Add(ByVal arg() As Control)
     System.Console.WriteLine(arg.Length)
  End Sub

Sin embargo, en Visual Studio 2010, los métodos de extensión también se distribuyen en varianza genérica. Esto puede representar un cambio importante, como se muestra en la figura 8, porque usted puede tener más candidatos de extensión que antes.

Figura 8 Cambio importante

Option Strict On
Imports System.Runtime.CompilerServices
Imports System.Windows.Forms
Module VarianceExample
    Sub Main()
        Dim _func As Func(Of Button) = Function() New Button
        ‘This was a compile-time error in VB9, But in VB10 because of variance convertibility, the compiler uses the extension method.
        _func.Add()
    End Sub

    <Extension()> _
    Public Sub Add(ByVal this As Func(Of Control))
        Console.WriteLine(“A call to func of Control”)
    End Sub
End Module

Conversiones definidas por el usuario

Visual Basic permite declarar conversiones en clases y estructuras, de modo que se puedan convertir hacia o desde otras clases y estructuras, así como también de tipos básicos. En Visual Studio 2010, la conversión de varianza ya está agregada en los algoritmos de conversión definida por el usuario. Por lo tanto, el ámbito de cada conversión definida por el usuario aumentará automáticamente, lo que podría producir problemas.

Puesto que Visual Basic y C# no permiten conversiones definidas por el usuario en las interfaces, sólo es necesario preocuparse con respecto a los tipos de delegados. Analice la conversión en la figura 9, que funciona en Visual Studio 2008, pero que genera un error en Visual Studio 2010.

Figura 9 Conversión que funciona en Visual Studio 2008, pero genera un error en Visual Studio 2010

Option Strict On
Imports System.Windows.Forms
Module VarianceExample
    Class ControlList
        Overloads Shared Widening Operator CType(ByVal arg As ControlList) As Func(Of Control)
            Console.WriteLine("T1->Func(Of Control)")
            Return Function() New Control
        End Operator
    End Class
    Class ButtonList
        Inherits ControlList
        Overloads Shared Widening Operator CType(ByVal arg As ButtonList) As Func(Of Button)
            Console.WriteLine("T2->Func(Of Button)")
            Return Function() New Button
        End Operator
    End Class
    Sub Main()
       'The conversion works in VB9 using ButtonList->ControlList->Func(Of Control)
'Variance ambiguity error in VB10, because there will be another widening path    (ButtonList-->Func(Of Button)--[Covariance]-->Func(Of Control)
        Dim _func As Func(Of Control) = New ButtonList
    End Sub
End Module

En la figura 10 se proporciona otro ejemplo de una conversión que generaría un error en tiempo de compilación en Visual Studio 2008 con Option Strict On, pero que será correcta en Visual Studio 2010 con conversión de varianza.

Figura 10 Visual Studio 2010 permite una conversión que antes era ilegal mediante el uso de conversión de varianza

Option Strict On
Imports System.Windows.Forms
Module VarianceExample
    Class ControlList
        Overloads Shared Narrowing Operator CType(ByVal arg As ControlList) As Func(Of Control)
            Return Function() New Control
        End Operator

        Overloads Shared Widening Operator CType(ByVal arg As ControlList) As Func(Of Button)
            Return Function() New Button
        End Operator
    End Class

    Sub Main()
‘This was an error in VB9 with Option Strict On, but the conversion will succeed in VB10 using Variance->Func(Of Button)-[Covariance]-Func(Of Control)
        Dim _func As Func(Of Control) = New ControlList
    End Sub
End Module

Efectos de Option Strict Off

Option Strict Off permite que las conversiones de restricción se realicen de forma implícita. Sin embargo, si Option Strict es On u Off, la conversión de varianza necesita que sus argumentos genéricos estén relacionados mediante amplitud compatible de asignación de CLR; no es suficiente para ellos estar relacionados mediante restricción (consulte la figura 11). Nota: consideramos T->U como restricción si existe una conversión de varianza U->T y consideramos T->U como restricción si T->U es ambiguo.

Figura 11 Con Option Strict Off

Option Strict Off
Imports System.Windows.Forms
Module VarianceExample
    Interface IEnumerable(Of Out Tout) 
    End Interface
    Class ControlList(Of T) 
       Implements IEnumerable(Of T) 
    End Class
    Sub Main()
        ‘No compile time error, but will throw Invalid Cast Exception at run time 
        Dim _ctrlList As IEnumerable(Of Button) = New ControlList(Of Control)
    End Sub
End Module

Restricciones para co y contravarianza

La siguiente es una lista de restricciones para co y contravarianza:

  1. Las palabras clave contextuales In/Out pueden aparecen sólo en declaraciones de interfaz o de delegado. Usar las palabras clave en cualquier otra declaración de parámetro genérico genera un error en tiempo de compilación. Una interfaz de varianza no puede anidar una clase o una estructura dentro de ésta, pero puede contener interfaces anidadas y delegados anidados, que luego tomará en una varianza desde el tipo contenedor.
  2. Sólo las interfaces y los delegados pueden ser covariantes o contravariantes y sólo cuando los argumentos de tipo sean tipos de referencia.
  3. La conversión de varianza no se puede realizar en interfaces de varianza cuya instancia se creó con tipos de valor.
  4. No se permite que las enumeraciones, las clases, los eventos y las estructuras entren en una interfaz de varianza. Esto se debe a que emitimos estas clases/estructuras/enumeraciones como genéricas, al heredar los parámetros genéricos de sus contenedores, de modo que éstas terminen por heredar la varianza de sus contenedores. Las clases, las estructuras y las enumeraciones de varianza están deshabilitadas por la especificación CLI.

Código más flexible y más limpio

Al trabajar con genéricos, en algunos casos, es posible que haya sabido que podía haber escrito código más simple o código más limpio si se hubiera admitido co y contravarianza. Ahora que estas características están implementadas en Visual Studio 2010 y .NET Framework 4, puede hacer que su código sea más limpio y más flexible al declarar propiedades de varianza en parámetros de tipo en interfaces genéricas y delegados genéricos.

Para facilitar esto, en .NET Framework 4, IEnumerable ahora es declarada covariante mediante el uso del modificador Out en su parámetro de tipo e IComparer es declarada contravariante mediante el uso del modificador In. Por lo tanto, para que IEnumerable(Of T) se pueda convertir a varianza en IEnumerable(Of U), debe cumplir con una de las siguientes condiciones:

  • T hereda U o
  • T se puede convertir a varianza en U o
  • T tiene cualquier otro tipo de conversión de referencia de CLR predefinida

En Basic Class Library, estas interfaces se declaran de la siguiente manera:

Interface IEnumerable(Of Out T)
  Function GetEnumerator() As IEnumerator(Of T)
End Interface
Interface IEnumerator(Of Out T) 
  Function Current() As T 
End Interface
Interface IComparer(Of In T) 
  Function Compare(ByVal arg As T, ByVal arg2 As T) As Integer 
End Interface
Interface IComparable(Of In T)
  Function CompareTo(ByVal other As T) As Integer
End Interface

Los parámetros de tipo de varianza, para ser con seguridad de tipos, los parámetros tipos sólo pueden aparecer como tipos de devolución o propiedades de sólo lectura (por ejemplo, pueden ser tipos de resultados, como en el método GetEnumerator y la propiedad Current anterior); los parámetros de tipo de covarianza pueden aparecer sólo como propiedades de parámetro o de sólo escritura (tipos de argumento, por ejemplo, como en los métodos Compare y CompareTo anteriores).

Las co y contravarianzas son características interesantes que eliminan ciertas inflexibilidades al trabajar con interfaces genéricas y delegados genéricos. Tener algún conocimiento básico acerca de estas características puede ser muy útil al escribir código que funciona con genéricos en Visual Studio 2010.

Binyam Kelile es ingeniero de diseño de software de prueba del equipo de lenguaje administrado de Microsoft. Durante el lanzamiento de VS 2008, trabajó en muchas de las características del lenguaje, que incluyen consultas LINQ y cierre léxico. Trabajó en varias características de co y contravarianza para la próxima versión de Visual Studio. Puede ponerse en contacto con él en binyamk@microsoft.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Beth Massi y Lucian Wischik