基本技術

Visual Basic 2010 中的泛型共變性與逆變性

Binyam Kelile

Visual Studio 2010 有一項針對泛型介面和委派而提供的新功能,稱為泛型共變性與逆變性。在 Visual Studio 2010 和 Microsoft .NET Framework 4 之前的版本中,泛型的運作方式一貫與子型別相關,因此不允許泛型類別與不同的型別引數進行轉換。

例如,如果您嘗試傳送 List(Of Derived) 給接受 IEnumerable(Of Base) 的方法,將會傳回錯誤。 但是 Visual Studio 2010 可以處理支援泛型介面和委派型別上的共變性與逆變性型別參數宣告的型別安全共變性與逆變性。在本文中,我將討論這項功能實際的意義,以及如何在您的應用程式中運用此功能。

由於按鈕是控制項,基於基本的物件導向繼承原則,下列的程式碼理當可以運作:

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

但是在 Visual Studio 2008 中卻不允許這麼做,它會產生「IEnumerable(Of Button) 無法轉換成 IEnumerable(Of Control)」的錯誤。然而身為物件導向程式設計師,我們都知道型別 Button 的值可以轉換成控制項,因此如前所述,根據基本繼承原則,此程式碼應該可行。

請看看下面範例:

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)

結果會失敗並出現 InvalidCastException,因為程式設計師將 IList(Of Button) 轉換成 IList(Of Control),接著插入根本不是按鈕的控制項。

Visual Studio 2010 可識別並允許類似第一個案例中的程式碼,但是以 .NET Framework 4 為對象時,可能仍不允許第二個案例中示範的這類程式碼。對大部分的使用者來說,程式通常會依照預期的方式運作,而且不需要追根究柢。不過在本文中,我將追根究柢說明程式碼運作的方式和可行的原因。

共變性

在將 IEnumerable(Of Button) 視為 IEnumerable(Of Control) 的第一個範例中,為何程式碼在 Visual Studio 2010 中是安全的,而在將 IList(Of Button) 視為 IList(Of Control) 的第二個程式碼範例中,卻不安全呢?

第一個案例之所以沒問題是因為 IEnumerable(Of T) 是「取出」介面,也就是說在 IEnumerable(Of Control) 中,介面使用者只能「取出」清單的控制項。

而第二個範例之所以不安全,是因為 IList(Of T) 是「放入取出」介面,因此在 IList(Of Control) 中,介面使用者可以「放入」也可以取出控制項。

Visual Studio 2010 中允許此作業的新語言功能稱為泛型共變性。在 .NET Framework 4 中,Microsoft 透過下列幾行程式碼改寫了架構:

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

IEnumerable(Of Out T) 中的 Out 附註表示如果 IEnumerable 中的方法提到 T,則只會在取出位置執行這個動作,例如針對函式的傳回或唯讀屬性的型別。這可讓使用者將任何 IEnumerable(Of Derived) 轉換成 IEnumerable(Of Base),而不會發生 InvalidCastException。

IList 缺少附註,因為 IList(Of T) 屬於放入取出介面。因此,使用者無法將 IList(Of Derived) 轉換成 IList(Of Base),反之亦然;執行這個動作將導致 InvalidCastException,如上所示。

逆變性

Out 附註有一個對照,但是稍微複雜一點,因此我先從範例講起:

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)

我在此建立了一個比較子,可用來判斷任兩個控制項是否相同,方法純粹是檢查控制項的名稱。

既然比較子可以比較「任何控制項」,當然也能夠評估剛好都是按鈕的兩個控制項。因此您可以安全地將它轉換成 IComparer(Of Button)。一般來說,您可以安全地將 IComparer(Of Base) 轉換成 IComparer(Of Derived)。這便是所謂的逆變性。作法是利用 In 附註,這完全是 Out 附註的對照。

經過修改的 .NET Framework 4 也加入了 In 泛型型別參數:

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

基於 IComparer(Of T) In 附註,IComparer 中每個提到 T 的方法都只會在放入位置執行這個動作,例如針對 ByVal 引數或唯寫屬性的型別。因此使用者可以將 IComparer(Of Base) 轉換成任何 IComparer(Of Derived),而不會發生 InvalidCastException。

我們來看一下 .NET 4 中的 Action 委派範例,且此委派中的 T 變成逆變性:

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

這個範例之所以在 .NET 4 中行得通,是因為委派 actionButton 的使用者始終會使用 Button 引數來叫用它,而這些引數都是控制項。

您也可以在自己的泛型介面中加入 In 和 Out 附註,然後進行委派。但是由於 Common Language Runtime (CLR) 的限制,您無法在類別、結構或其他地方使用這些附註。簡而言之,只有介面與委派可以是共變性或逆變性。

宣告/語法

Visual Basic 使用兩個新的內容關鍵字:產生共變性的 Out,以及以相同方式產生逆變性的 In,正如以下範例所示:

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

但是為何需要這兩個內容關鍵字或語法呢?我們為什麼不自動推斷變異數 In/Out?首先,這可以方便程式設計師宣告本身的意圖。其次,有時編譯器無法自動推斷最佳的變異數附註。

我們來看一下 IReadWriteBase 與 IReadWrite 這兩個介面:

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

如果編譯器推斷兩者都是 Out (如下所示),則程式碼即可順利運作:

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

如果編譯器推斷兩者都是 In (如下所示),則程式碼同樣可順利運作:

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

編譯器不知道該選 In 或 Out,因此提供一個語法。

Out/In 內容關鍵字只出現在介面和委派宣告中。在其他泛型參數宣告中使用這些關鍵字將導致編譯階段錯誤。Visual Basic 編譯器不允許變異介面包含巢狀列舉、類別及結構,因為 CLR 不支援 Variant 類別。不過您可以在類別內部將變異介面作巢狀處理。

處理語意模糊

共變性與逆變性在成員查詢中會產生語意模糊的情形,因此您應該了解造成語意模糊的原因,以及 Visual Basic 編譯器處理的方式。

我們來看一下 [圖 1] 中的範例,在這個範例中,我們嘗試將 Comparer 轉換成 IComparer(Of Control),且 Comparer 會實作 IComparer(Of Button) 和 IComparer(Of CheckBox)。

[圖 1] 語意模糊轉換

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

由於 IComparer(Of Button) 和 IComparer(Of CheckBox) 皆可變異轉換成 IComparer(Of Control),因此轉換將產生語意模糊。結果是,Visual Basic 編譯器根據 CLR 規則尋找語意模糊,如果 Option Strict 為 On,在編譯階段就不允許這類語意模糊轉換;如果 Option Strict 為 Off,編譯器便會產生警告。

[圖 2] 中的轉換在執行階段成功,並且未產生編譯階段錯誤。

[圖 2] 在執行階段成功的轉換

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

[圖 3] 說明同時實作 IComparer(of IBase) 與 IComparer(of IDerived) 泛型介面的危險。此處的 Comparer1 與 Comparer2 根據不同順序以不同的泛型型別參數來實作相同的變異泛型介面。除了實作介面時的順序不同外,Comparer1 與 Comparer2 是一模一樣,儘管如此,在這些類別中呼叫 Compare 方法會產生不同的結果。

[圖 3] 相同方法產生不同結果

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

以下說明為何 _comp 與 _comp2 一模一樣,但 [圖 3] 中的程式碼卻產生不同的結果。編譯器直接發出執行轉換的 Microsoft Intermediate Language。因此考慮到 Compare() 方法的實作不同,究竟要取 IComparer(Of IAccountRoot) 或 IComparer(Of IAccount) 介面的選擇就落到 CLR 頭上,而 CLR 永遠是挑選介面清單中第一個與指定相容的介面。因此在 [圖 3] 的程式碼中,Compare() 方法會提供不同的結果,因為 CLR 為 Comparer1 類別選擇 IComparer(Of IAccountRoot) 介面,為 Comparer2 類別選擇 IComparer(Of IAccount) 介面。

泛型介面的條件約束

當您撰寫泛型條件約束 — (Of T As U, U) — As 現在除了繼承以外,也要包含變異數可轉換性。[圖 4] 顯示 (of T As U, U) 包含變異數可轉換性。

[圖 4] 泛型條件約束如何包含變異數可轉換性

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

變異數泛型參數可以限定為不同的變異數參數。請參考下列範例:

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

在這個範例中,IEnumerator(Of ButtonBase, ButtonBase) 可以轉換變異數成 IEnumerator(Of Control, Button),而 IEnumerable(Of Control, Button) 可以轉換成 IEnumerable(Of ButtonBase, ButtonBase),而且仍然滿足條件約束。它理論上是可以進一步轉換變異數成 IEnumerable(Of ButtonBase, Control),但此時不再符合條件約束,因此這不是有效的型別。[圖 5] 代表適合使用條件約束的先進先出物件集合。

[圖 5] 適合使用條件約束的物件集合

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

[圖 5] 中,如果我給您 _IPipe,您只能將 Button 推入管道,而且您只能讀取其中的 Control。要注意的是,有鑒於介面永遠不允許變異數轉換,您可以將變異數介面限定為某種值型別。下列是泛型參數中使用值型別條件約束的範例:

Interface IEnumerable(Of Out Tout As Structure)
End Interface

考慮到繼承值型別的變異數介面不允許變異數轉換,限定為值型別可能沒有用。但是請注意,由於 Tout 是結構,透過條件約束間接推斷型別可能蠻有用的。

函式的泛型參數上的條件約束

方法/函式中的條件約束必須具有 In 型別。關於函式的泛型參數上的條件約束,有兩種基本想法:

  • 在大部分的情況下,函式的泛型參數基本上是函式的輸入,而且所有輸入都必須具有 In 型別。
  • 用戶端可以將 Out 型別轉換變異數成 System.Object。如果泛型參數限定成某種 Out 型別,那麼用戶端實際上可以移除該條件約束,而這並不符合條件約束的本意。

我們來看一下 [圖 6],其中清楚指出條件約束缺乏此變異數有效性會發生什麼情況。

[圖 6] 條件約束缺乏變異數有效性會發生什麼情況

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

[圖 6] 中,我們在 m_data 內部將 Label 儲存成 Button,這是不合法的。因此,方法/函式中的條件約束必須具有 In 型別。

多載解析

多載是指建立多個具有相同名稱但接受不同引數型別的函式。多載解析是一種解譯階段機制,用來從一組候選項目中選取最佳函式。

我們來看看下面的例子:

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

Foo(2)

幕後到底發生了什麼事?當編譯器看到呼叫 Foo(2),它必須搞清楚您要叫用哪個 Foo。為了這麼做,編譯器使用以下簡單的演算法:

  1. 透過查詢所有包含名稱 Foo 的項目來產生一組所有適合的候選項目。在我們的範例中,有兩個候選項目列入考量。
  2. 針對個別候選項目,查看引數並移除不適用的函式。請注意,編譯器也會執行一些驗證和泛型的型別推斷。

引入變異數之後,便展開一組預先定義的轉換,結果是步驟 2 將接受更多候選函式, 比以前更多。此外,如果原本有兩個同樣獨特的候選項目,編譯器會挑選未遮蔽的項目,但如今遮蔽項目可能範圍更廣,因此編譯器反而可能挑選它。[圖 7] 顯示的是可能中斷將變異數加入 Visual Basic 的程式碼。

[圖 7] 可能中斷將變異數加入 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

在 Visual Studio 2008 中,呼叫 Add 將繫結到 Object,但是有了 Visual Studio 2010 的變異數可轉換性,我們改用 IEnumerable(Of Control)。

編譯器只有在沒有其他候選項目時,才會選擇縮小 (Narrowing) 候選項目,但是藉由變異數可轉換性,如果出現新的擴展 (Widening) 候選項目,編譯器反而會選擇此項目。如果變異數可轉換性導致另一個新的縮小候選項目,則編譯器會發出錯誤。

延伸方法

延伸方法可讓您將方法加入現有型別,而不必建立新的衍生型別、重新編譯或者修改原始型別。在 Visual Studio 2008 中,延伸方法支援陣列共變性,如以下範例所示:

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

但是在 Visual Studio 2010 中,延伸方法也會分派在泛型變異數上。這是一項重大改變,如 [圖 8] 所示,因為您的延伸候選項目可能比以前更多。

[圖 8] 重大改變

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

使用者定義轉換

Visual Basic 可讓您在類別和結構上宣告轉換,以便將它們轉換成其他類別和結構以及基本型別,或是從其他類別、結構及基本型別轉換成您的類別和結構。在 Visual Studio 2010 中,變異數可轉換性已經加入使用者定義轉換演算法中。因此,每個使用者定義轉換的範圍都會自動增加,而可能會造成中斷。

Visual Basic 和 C# 不允許在介面上進行使用者定義轉換,因此我們只需要關心委派型別的部分。試想一下 [圖 9] 中的轉換,在 Visual Studio 2008 中可行,但在 Visual Studio 2010 中卻造成錯誤。

[圖 9] 在 Visual Studio 2008 中可行但在 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

[圖 10] 提供另一個轉換範例,它在 Option Strict On 的 Visual Studio 2008 中會導致編譯階段錯誤,但是在具有變異數可轉換性的 Visual Studio 2010 中成功執行。

[圖 10] Visual Studio 2010 藉由使用變異數可轉換性來允許原先不合法的轉換

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

Option Strict Off 的影響

Option Strict Off 通常允許以隱含方式執行縮小轉換。但是無論 Option Strict 是 On 或 Off,變異數可轉換性都需要其泛型引數透過 CLR 的指定相容擴展建立相關性;透過縮小來建立相關性是不夠的 (請參閱 [圖 11])。注意事項:我們將 T->U 視為縮小的前提是要有變異數轉換 U->T,如果 T->U 語意模糊,我們也會將 T->U 視為縮小。

[圖 11] 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

共變性與逆變性的限制

以下列出共變性與逆變性的限制:

  1. In/Out 內容關鍵字可以出現在介面宣告或委派宣告中。在其他泛型參數宣告中使用這些關鍵字將導致編譯階段錯誤。變異數介面不能巢狀處理其內部的類別或結構,但是可以包含巢狀介面和巢狀委派,接著再從包含型別採取變異數。
  2. 只有介面和委派可以是共變性或逆變性,而且僅限於型別引數為參考型別的情況。
  3. 變異數轉換不可以在使用值型別具現化的變異數介面上執行。
  4. 不允許列舉、類別、事件和結構進入變異數介面內部。這是因為我們將這些類別/結構/列舉發出成泛型,並繼承其容器的泛型參數,因此它們最終會繼承容器的變異數。CLI 規格不允許變異數類別/結構/列舉。

更富彈性、更簡潔的程式碼

使用泛型時,您可能已經透過某些案例發現如果支援共變性與逆變性,您可能可以撰寫更簡單、更清楚的程式碼。現在這些功能已經實作在 Visual Studio 2010 與 .NET Framework 4 中,只要在泛型介面和委派的型別參數上宣告變異數屬性,您就可以讓程式碼更簡潔且更有彈性。

為達到此目的,在 .NET Framework 4 中,IEnumerable 現在是在型別參數中使用 Out 修飾詞來宣告共變性,IComparer 則是使用 In 修飾詞來宣告逆變性。因此 IEnumerable(Of T) 如果要轉換變異數成 IEnumerable(Of U),您必須符合下列其中一項條件:

  • T 從 U 繼承,或者
  • T 可轉換變異數成 U,或者
  • T 具有任何其他種預先定義的 CLR 參考轉換

在 Basic Class Library 中,這些介面都以下列方式宣告:

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

為了型別安全,共變性型別參數只能當作傳回型別或唯讀屬性來顯示 (例如,它們可以是結果型別、正如上述的 GetEnumerator 方法與 Current 屬性);逆變性型別參數只能當作參數或唯寫屬性來顯示 (例如,上述 Compare 和 CompareTo 方法中的引數型別)。

共變性與逆變性是很有趣的功能,可以消除使用泛型介面和委派時遇到的一些缺乏變通的問題。對於這些功能具有基本了解在撰寫使用 Visual Studio 2010 的泛型的程式碼將很有幫助。

Binyam Kelile* 是一位 Microsoft Managed 語言測試小組的軟體設計工程師。在發行 VS 2008 版本的過程中,他負責多項語言功能,包括 LINQ 查詢和語彙關閉 (Lexical Closure)。針對即將發行的 Visual Studio 版本,他則參與了共變性與逆變性功能的工作。您可以透過電子郵件與他連絡:binyamk@microsoft.com。*

感謝以下協助校閱本篇文章的技術專家:Beth Massi、Lucian Wischik