Basic Instincts

Generische Ko- und Kontravarianz in Visual Basic 2010

Binyam Kelile

Visual Studio 2010 bietet ein neues Feature namens generische Ko- und Kontravarianz, das bei der Arbeit mit generischen Schnittstellen und Delegaten zur Verfügung steht. In Vorgängerversionen von Visual Studio 2010 und Microsoft .NET Framework 4 verhalten sich generische Typen invariant in Bezug auf Untertypen, und daher sind hier Konvertierungen zwischen generischen Typen mit verschiedenen Typargumenten nicht zulässig.

Wenn beispielsweise ein Argument vom Typ List(Of Derived) einer Methode übergeben wird, die Argumente vom Typ IEnumerable(Of Base) akzeptiert, tritt ein Fehler auf. Visual Studio 2010 kann jedoch typsichere Ko- und Kontravarianz handhaben, sodass die Deklaration von kovarianten und kontravarianten Typparametern für generischen Schnittstellen- und Delegatentypen zulässig ist. In diesem Artikel wird erläutert, was dieses Feature eigentlich bedeutet und wie Sie es in Anwendungen nutzen können.

Weil eine Schaltfläche ein Steuerelement ist, würde man erwarten, dass der folgende Code wegen der grundlegenden objektorientierten Vererbungsprinzipien funktioniert:

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

In Visual Studio 2008 ist er allerdings nicht zulässig und führt zu der Fehlermeldung "IEnumerable(Of Button) kann nicht in IEnumerable(Of Control) konvertiert werden". Als objektorientierte Programmierer wissen wir aber, dass ein Wert vom Typ Button in ein Steuerelement konvertiert werden kann, und daher sollte dieser Code, wie bereits erwähnt, den grundlegenden Vererbungsprinzipien entsprechend eigentlich zulässig sein.

Betrachten wir folgendes Beispiel:

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)

Dieser Code würde nicht ausgeführt und die Ausnahme InvalidCastException erzeugen, weil der Programmierer den Typ IList(Of Button) in den Typ IList(Of Control) konvertiert hat und dann ein Steuerelement in etwas eingefügt hat, das noch nicht einmal vom Typ Button war.

Visual Studio 2010 erkennt solchen Code wie im ersten Beispiel und lässt ihn zu. Der im zweiten Beispiel dargestellte Code wäre aber auch hier nicht zulässig, wenn .NET Framework 4 als Ziel gewählt wird. Für die meisten Benutzer und die meiste Zeit funktionieren Programme einfach so wie erwartet, und es ist nicht erforderlich, sich eingehender damit auseinanderzusetzen. In diesem Artikel möchte ich aber genauer hinsehen und erklären, wie und warum Code funktioniert.

Kovarianz

Warum ist der Code aus dem ersten Codelisting, in dem IEnumerable(Of Button) als IEnumerable(Of Control) betrachtet wurde, in Visual Studio 2010 sicher, und das zweite Codebeispiel, in dem IList(Of Button) als IList(Of Control) betrachtet wurde, dagegen unsicher?

Der erste Code ist in Ordnung, weil IEnumerable(Of T) eine Ausgabeschnittstelle ist. Das heißt, dass Benutzer der Schnittstelle IEnumerable(Of Control) nur Steuerelemente aus der Liste entnehmen können.

Der zweite Code ist unsicher, weil IList(Of T) eine Ein- und Ausgabeschnittstelle ist. Das heißt, dass die Benutzer der Schnittstelle IList(Of Control) Steuerelemente sowohl einfügen als auch der Liste entnehmen können.

Das neue Sprachfeature von Visual Studio 2010, das dies ermöglicht, heißt generische Kovarianz. In .NET Framework 4 hat Microsoft das Framework entsprechend geändert:

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

Die Out-Anmerkung in IEnumerable(Of Out T) zeigt an, dass Methoden von IEnumerable den Typ T nur in Ausgabepositionen verwenden, beispielsweise als Rückgabetyp einer Funktion oder als Typ einer schreibgeschützten Eigenschaft. Daher kann der Typ IEnumerable(Of Derived) in den Typ IEnumerable(Of Base) umgewandelt werden, ohne dass eine Ausnahme des Typs InvalidCastException auftritt.

Bei IList fehlt diese Anmerkung, weil IList(Of T) eine Ein- und Ausgabeschnittstelle ist. Folglich kann der Typ IList(Of Derived) weder in den Typ IList(Of Base) umgewandelt werden noch umgekehrt der Typ IList(Of Base) in den Typ IList(Of Derived). Wie wir oben gesehen haben, wird andernfalls eine Ausnahme des Typs InvalidCastException ausgelöst.

Kontravarianz

Es gibt ein Gegenstück zur Out-Anmerkung. Da diese Anmerkung etwas subtiler ist, beginne ich mit einem Beispiel:

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)

Hier habe ich eine Vergleichskomponente erstellt, die ermitteln kann, ob zwei Steuerelemente identisch sind, indem sie nur deren Namen vergleicht.

Weil diese Vergleichskomponente Steuerelemente jeder Art vergleichen kann, ist sie sicherlich in der Lage, zwei Steuerelemente zu bewerten, die zufällig Schaltflächen sind. Daher ist die Typumwandlung in IComparer(Of Button) unbedenklich. Allgemein gesagt, kann der Typ IComparer(Of Base) gefahrlos in jeden Typ IComparer(Of Derived) umgewandelt werden. Dies wird Kontravarianz genannt. Ermöglicht wird dies durch die In-Anmerkung, die das genaue Gegenstück der Out-Anmerkung darstellt.

.NET Framework 4 wurde auch dahingehend modifiziert, dass generische In-Typparameter unterstützt werden:

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

Wegen der In-Anmerkung bei IComparer(Of T) verwendet jede Methode von IComparer, die auf T Bezug nimmt, T in einer Eingabeposition, beispielsweise als ByVal-Argument oder als Typ einer lesegeschützten Eigenschaft. Daher kann der Typ IComparer(Of Base) in den Typ IComparer(Of Derived) umgewandelt werden, ohne dass eine Ausnahme des Typs InvalidCastException auftritt.

Sehen wir uns ein Beispiel mit dem Action-Delegaten aus .NET 4 an, in dem der Delegat in T kontravariant wird:

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

Das Beispiel funktioniert in .NET 4, weil der Benutzer des Delegaten actionButton diesen immer mit Button-Argumenten aufruft, die Steuerelemente sind.

Sie können auch eigenen generischen Schnittstellen und Delegaten die Anmerkungen In und Out hinzufügen. Aufgrund von Beschränkungen der CLR (Common Language Runtime) können diese Anmerkungen jedoch nicht für Klassen, Strukturen oder andere Dinge verwendet werden. Kurz gesagt, nur Schnittstellen und Delegate können ko- oder kontravariant.

Deklarationen/Syntax

Visual Basic kennt zwei neue kontextabhängige Schlüsselwörter: Out, womit Kovarianz eingeführt wird, und In, womit Kontravarianz eingeführt wird, wie im folgenden Beispiel veranschaulicht wird:

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

Warum brauchen wir diese beiden kontextabhängigen Schlüsselwörter bzw. diese Syntax überhaupt? Warum wird nicht automatisch die Varianz In/Out abgeleitet? Erstens ist es sinnvoll, wenn Programmierer ihre Absichten deklarieren. Zweitens gibt es Situationen, in denen der Compiler nicht automatisch auf die am besten geeignete Varianzanmerkung schließen kann.

Betrachten wir die beiden Schnittstellen IReadWriteBase und IReadWrite:

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

Wenn der Compiler folgert, dass beide Out sein sollen, dann funktioniert der Code einwandfrei:

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

Und wenn der Compiler folgert, dass beide In sind, funktioniert der Code auch einwandfrei:

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

Der Compiler kann nicht wissen, welche Anmerkung (In oder Out) er wählen soll, und stellt daher eine Syntax bereit.

Die kontextabhängigen Schlüsselwörter Out/In werden nur in Deklarationen von Schnittstellen und Delegaten verwendet. Wenn die Schlüsselwörter in irgendeiner anderen generischen Parameterdeklaration verwendet werden, tritt während der Kompilierung ein Fehler auf. Der Visual Basic-Compiler lässt nicht zu, dass variante Schnittstellen geschachtelte Enumerationen, Klassen und Strukturen enthält, weil die CLR variante Klassen nicht unterstützt. Variante Schnittstellen können allerdings in einer Klasse verschachtelt werden.

Umgang mit Ambiguität

Ko- und Kontravarianz führen zu Mehrdeutigkeiten beim Nachschlagen von Membern. Sie sollten daher wissen, wodurch diese Ambiguität bedingt wird und wie der Visual Basic-Compiler damit umgeht.

Sehen wir uns das Beispiel in Abbildung 1 an, in dem wir versuchen, den Typ Comparer in IComparer(Of Control) zu konvertieren, wobei Comparer die Schnittstellen IComparer(Of Button) und IComparer(Of CheckBox) implementiert.

Abbildung 1 Eine mehrdeutige Konvertierung

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

Weil sowohl IComparer(Of Button) als auch IComparer(Of CheckBox) in IComparer(Of Control) variant konvertierbar sind, ist die Konvertierung mehrdeutig. Der Visual Basic-Compiler überprüft den Code gemäß den CLR-Regeln auf Ambiguitäten und wenn für die Option Strict die Einstellung On gewählt wurde, werden solche mehrdeutigen Konvertierungen nicht kompiliert. Wenn für die Option Strict die Einstellung Off festgelegt wurde, gibt der Compiler eine Warnung aus.

Die Konvertierung in Abbildung 2 wird ausgeführt und erzeugt während der Kompilierung keinen Fehler.

Abbildung 2 Eine Konvertierung, die zur Laufzeit erfolgreich ausgeführt wird

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

Abbildung 3 veranschaulicht die Gefahren, die eine Implementierung der beiden generischen Schnittstellen IComparer(of IBase) und IComparer(of IDerived) birgt. Hier implementieren die Klassen Comparer1 und Comparer2 die gleiche generische Schnittstelle, wobei verschiedene generische Typparameter in unterschiedlicher Reihenfolge angegeben werden. Obwohl Comparer1 und Comparer2 identisch sind, abgesehen von der Reihenfolge bei der Implementierung der Schnittstelle, liefern Aufrufe der Compare-Methode in diesen Klassen verschiedene Ergebnisse.

Abbildung 3 Dieselbe Methode liefert verschiedene Ergebnisse

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

Nachfolgend wird erklärt, warum der Code aus Abbildung 3, zu unterschiedlichen Ergebnissen führt, obwohl _comp und _comp2 identisch sind. Der Compiler gibt einfach Microsoft Intermediate Language-Code aus, der die Typumwandlung durchführt. Daher wird die Entscheidung, ob die Schnittstelle IComparer(Of IAccountRoot) oder IComparer(Of IAccount) abgerufen werden soll, wenn eine andere Implementierung der Compare()-Methode gegeben ist, der CLR überlassen, die immer die erste zuweisungskompatible Schnittstelle in der Schnittstellenliste auswählt. Daher liefert die Compare()-Methode im Code aus Abbildung 3 ein anderes Ergebnis, weil die CLR die IComparer(Of IAccountRoot)-Schnittstelle für die Comparer1-Klasse und die IComparer(Of IAccount)-Schnittstelle für die Comparer2-Klasse auswählt.

Einschränkungen einer generischen Schnittstelle

In der Definition einer generischen Einschränkung, beispielsweise (Of T As U, U), umfasst As jetzt neben der Vererbung auch Varianzkonvertierbarkeit. Abbildung 4 zeigt, dass (of T As U, U) Varianzkonvertierbarkeit beinhaltet.

Abbildung 4 Eine generische Einschränkung beinhaltet Varianzkonvertierbarkeit

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

Ein varianter generischer Parameter kann auf einen anderen varianten Parameter eingeschränkt werden. Betrachten Sie folgendes Beispiel:

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

In diesem Beispiel kann der Typ IEnumerator(Of ButtonBase, ButtonBase) variant in den Typ IEnumerator(Of Control, Button) konvertiert und der Typ IEnumerable(Of Control, Button) kann in den Typ IEnumerable(Of ButtonBase, ButtonBase) konvertiert werden, wobei die Einschränkungen gewahrt bleiben. Theoretisch könnte der Typ weiter in den Typ IEnumerable(Of ButtonBase, Control) konvertiert werden. Da die Einschränkungen dann aber nicht mehr erfüllt würden, ist dies kein gültiger Typ. Abbildung 5 stellt eine FIFO-Auflistung (First-in-First-out) von Objekten dar, bei der eine Einschränkung hilfreich sein könnte.

Abbildung 5 Wo eine Einschränkung in einer Auflistung von Objekten hilfreich ist

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

In Abbildung 5 wird _IPipe bereitgestellt; nur der Typ Button kann in die Pipe eingefügt und nur der Typ Control kann daraus gelesen werden. Beachten Sie, dass Sie eine variante Schnittstelle auf einen Werttyp beschränken können, sofern diese Schnittstelle keine Varianzkonvertierungen zulässt. Es folgt ein Beispiel mit einer Werttypeinschränkung für einen generischen Parameter:

Interface IEnumerable(Of Out Tout As Structure)
End Interface

Eine Einschränkung auf einen Werttyp mag unsinnig sein, da bei einer varianten Schnittstelle, die mit einem Werttyp instanziiert wird, ohnehin keine Varianzkonvertierungen zulässig sind. Da Tout jedoch eine Struktur ist, kann es sinnvoll sein, indirekt über Einschränkungen auf den Typ zu schließen.

Einschränkungen für die generischen Parameter einer Funktion

Einschränkungen von Methoden/Funktionen müssen Eingabetypen haben. Grundsätzlich können Einschränkungen für die generischen Parameter einer Funktion auf zweierlei Weise betrachtet werden:

  • In den meisten Fällen ist ein generischer Funktionsparameter im Grunde eine Eingabe für die Funktion, und alle Eingaben müssen Eingabetypen aufweisen.
  • Ein Client kann jeden Ausgabetyp in den varianten Typ System.Object konvertieren. Wenn ein generischer Parameter auf einen Ausgabetyp eingeschränkt wird, dann kann ein Client diese Einschränkung entfernen, und das ist nicht im Sinn der Sache.

Sehen wir uns Abbildung 6 an, die verdeutlicht, was ohne diese Varianzgültigkeitsregel für Einschränkungen passieren würde.

Abbildung 6 Was geschieht, wenn es keine Varianzgültigkeitsregel für Einschränkungen gibt

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

In Abbildung  6 wird ein Label-Element als Button in m_data gespeichert, was nicht zulässig ist. Daher müssen Einschränkungen von Methoden/Funktionen Eingabetypen aufweisen.

Auflösen von Überladungen

Mit dem Begriff Überladen ist das Erstellen mehrerer gleichnamiger Funktionen gemeint, die unterschiedliche Argumenttypen akzeptieren. Das Auflösen von Überladungen ist ein Mechanismus, der während der Kompilierung eingesetzt wird und dazu dient, die besten Funktionen aus einem Satz von Kandidaten auszuwählen.

Betrachten wir das folgende Beispiel:

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

Foo(2)

Was geschieht hier eigentlich hinter den Kulissen? Wenn der Compiler auf den Aufruf von Foo(2) stößt, muss er herausfinden, welche Foo-Methode aufgerufen werden soll. Dazu verwendet er den folgenden einfachen Algorithmus:

  1. Erzeuge eine Menge mit allen geeigneten Kandidaten, indem nach allem gesucht wird, das den Namen Foo hat. In unserem Beispiel kommen zwei Kandidaten in Betracht.
  2. Überprüfe bei jedem Kandidaten die Argumente und entferne nicht geeignete Funktionen. Beachten Sie, dass der Compiler bei generischen Typen Argumente auch verifiziert und den Typ ableitet.

Mit der Einführung der Varianz wird die Menge vordefinierter Konvertierungen erweitert, infolgedessen werden in Schritt 2 mehr geeignete Funktionen akzeptiert als zuvor. Außerdem gilt, dass früher in Fällen, in denen zwei gleichermaßen gut geeignete Kandidaten gegeben waren, der Compiler den Kandidaten gewählt hätte, für den kein Shadowing durchgeführt wurde. Heute würde ein Kandidat durch das Shadowing jedoch unter Umständen allgemeiner, und der Compiler würde dann diesen Kandidaten wählen. Abbildung 7 zeigt Code, der möglicherweise durch die Hinzufügung von Varianz in Visual Basic nicht mehr ausgeführt würde.

Abbildung 7 Code, der möglicherweise durch die Hinzufügung von Varianz in Visual Basic nicht mehr ausgeführt würde

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

In Visual Studio 2008 wird der Aufruf von Add an Object gebunden, aber dank der Varianzkonvertierbarkeit von Visual Studio 2010 verwenden wir stattdessen IEnumerable(Of Control).

Der Compiler wählt nur dann einen einschränkenden Kandidaten aus, wenn kein anderer Kandidat verfügbar ist. Ist ein anderer allgemeinerer Kandidat vorhanden, dann wählt der Compiler diesen aus. Wenn sich durch die Varianzkonvertierbarkeit ein weiterer neuer einschränkender Kandidat ergibt, gibt der Compiler einen Fehler aus.

Erweiterungsmethoden

Erweiterungsmethoden ermöglichen es Ihnen, Methoden zu vorhandenen Typen hinzuzufügen, ohne neue abgeleitete Typen definieren, den Code neu kompilieren oder den ursprünglichen Typ auf andere Weise ändern zu müssen. In Visual Studio 2008 unterstützen Erweiterungsmethoden Arraykovarianz, wie im folgenden Beispiel dargestellt wird:

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

Dagegen liefern Erweiterungsmethoden in Visual Studio 2010 auch bei generischer Varianz. Wie in Abbildung 8 gezeigt, kann dies eine bedeutende Änderung darstellen, da danach mehr Erweiterungskandidaten als zuvor gegeben sind.

Abbildung 8 Eine bedeutende Änderung

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

Benutzerdefinierte Konvertierungen

Visual Basic erlaubt es, Konvertierungen für Klassen und Strukturen zu deklarieren, sodass diese in andere Klassen und Strukturen sowie grundlegende Typen konvertiert werden können. In Visual Studio 2010 wurden benutzerdefinierte Konvertierungsalgorithmen bereits durch Varianzkonvertierbarkeit ergänzt. Daher wird der Bereich jeder benutzerdefinierten Konvertierung automatisch erweitert, was zu Fehlern führen kann.

Weil in Visual Basic und C# benutzerdefinierte Konvertierungen bei Schnittstellen nicht zulässig sind, müssen wir uns nicht um Delegattypen kümmern. Betrachten Sie die Konvertierung in Abbildung 9, die in Visual Studio 2008 funktioniert, in Visual Studio 2010 jedoch einen Fehler verursacht.

Abbildung 9 Eine Konvertierung, die in Visual Studio 2008 funktioniert, in Visual Studio 2010 jedoch einen Fehler verursacht

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

Abbildung 10 enthält ein weiteres Beispiel für eine Konvertierung, die in Visual Studio 2008 während der Kompilierung einen Fehler verursacht, wenn "Option Strict On" angegeben wurde, in Visual Studio 2010 aber dank der Varianzkonvertierbarkeit fehlerfrei ausgeführt wird.

Abbildung 10 Dank Varianzkonvertierbarkeit sind in Visual Studio 2010 früher unzulässige Konvertierungen zulässig

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

Auswirkungen der Einstellung "Option Strict Off"

Normalerweise lässt die Einstellung "Option Strict Off" zu, dass einschränkende Konvertierungen implizit durchgeführt werden. Aber ungeachtet dessen, ob "Option Strict On" oder "Option Strict Off" angegeben wurde, erfordert die Varianzkonvertierbarkeit, dass die generischen Argumente unter dem Gesichtspunkt der zuweisungskompatiblen Erweiterung der CLR verwandt sind; es ist nicht ausreichend, wenn sie über Einschränkungen verwandt sind (siehe Abbildung 11). Hinweis: Hier wird T->U nicht als Einschränkung betrachtet, wenn es eine Varianzkonvertierung U->T gibt, und wir betrachten T->U als Einschränkung, wenn T->U mehrdeutig ist.

Abbildung 11 Mit Einstellung "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

Einschränkungen für Ko- und Kontravarianz

Es folgt eine Liste der Einschränkungen hinsichtlich Ko- und Kontravarianz.

  1. Die kontextabhängigen Schlüsselwörter Out/In dürfen nur in Deklarationen von Schnittstellen oder Delegaten verwendet werden. Wenn die Schlüsselwörter in irgendeiner anderen generischen Parameterdeklaration verwendet werden, tritt während der Kompilierung ein Fehler auf. Eine variante Schnittstelle kann keine verschachtelte Klasse oder Struktur enthalten, kann aber verschachtelte Schnittstellen und verschachtele Delegaten aufweisen, die die Varianz vom übergeordneten Typ übernehmen.
  2. Nur Schnittstellen und Delegate mit Referenztypen als Argumenttypen können ko- oder kontravariant sein.
  3. Bei varianten Schnittstellen, die mit Werttypen instanziiert werden, können keine Varianzkonvertierungen durchgeführt werden.
  4. Enumerationen, Klassen, Ereignisse und Strukturen dürfen nicht in eine variante Schnittstelle eingefügt werden. Der Grund hierfür ist, dass wir diese Klassen/Strukturen/Enumerationen als generische Typen ausgeben, die die generischen Parameter ihrer Container erben. Daher würden sie die Varianz ihres Containers erben. Variante Klassen/Strukturen/Enumerationen sind laut CLI-Spezifikation nicht zulässig.

Flexiblerer, sauberer Code

Wenn Sie mit generischen Typen arbeiten, werden Sie gelegentlich bemerkt haben, dass Sie einfacheren oder saubereren Code hätten schreiben können, wenn Ko- oder Kontravarianz unterstützt worden wäre. Nachdem diese Features in Visual Studio 2010 und .NET Framework 4 implementiert wurden, können Sie Ihren Code viel reiner und flexibler gestalten, indem Sie in generischen Schnittstellen und Delegaten Varianzeigenschaften für Typparameter deklarieren.

Um dies zu erleichtern, wurde in .NET Framework 4 der Typ IEnumerable als kovariant deklariert, indem im Typparameter der Out-Modifizierer angegeben wurde, und IComparer wurde durch Angabe des In-Modifizierers als kontravariant deklariert. Wenn IEnumerable(Of T) also varianzkonvertierbar in IEnumerable(Of U) sein soll, muss eine der folgenden Bedingungen erfüllt sein:

  • Entweder T erbt von U oder
  • T ist varianzkonvertierbar in U oder
  • T verfügt über eine andere Art von vordefinierter CLR-Referenzkonvertierung

In der Basic-Klassenbibliothek sind diese Schnittstellen wie folgt deklariert:

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

Damit sie typsicher sind, dürfen kovariante Typparameter nur als Rückgabetypen oder schreibgeschützte Eigenschaften verwendet werden (z. B. können sie Rückgabetypen sein, wie oben bei der GetEnumerator-Methode und der Current-Eigenschaft). Kontravariante Typparameter dürfen nur als Parameter oder lesegeschützte Eigenschaften verwendet werden (beispielsweise Argumenttypen wie oben bei den Methoden Compare und CompareTo).

Ko- und Kontravarianz sind interessante Features, die bei der Arbeit mit generischen Schnittstellen und Delegaten zur Verfügung bestimmte Inflexibilitäten aus dem Weg räumen. Grundkenntnisse dieser Features können beim Schreiben von Code sehr hilfreich sein, indem in Visual Studio 2010 generische Typen verwendet werden.

Binyam Kelile* ist im Microsoft Managed Language Team als Software Design Engineer in Test tätig. Bei der Veröffentlichung von Visual Basic 2008 hat er an vielen der Sprachfeatures mitgearbeitet, wie z. B. LINQ-Abfragen und Lexical Closure. Er hat an den Features Ko- und Kontravarianz für die kommende Visual Studio-Version mitgewirkt. Er ist zu erreichen unter binyamk@microsoft.com.*

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Beth Massi, Lucian Wischik