基本技术

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,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)。这称为逆变。这是通过与 Out 批注相对的 In 批注实现的。

.NET Framework 4 也进行了修改以引入 In 泛型类型参数:

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

因为 IComparer(Of T) In 批注的存在,IComparer 中用到 T 的每个方法都只在输入位置使用 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 参数调用它,而 Button 参数是控件。

您也可以向自己的泛型接口和委托添加 In 和 Out 批注。但由于公共语言运行时 (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 不支持变体类。不过可以在类中嵌套变体接口。

处理多义性

协变和逆变会在成员查询中造成多义性,因此,应该知道是什么引起多义性以及 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 中间语言。因此,如果 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) 时,现在除继承外还包含了变化可转换性。图 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 中,Label 在 m_data 中存储为 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)。

仅当没有其他候选函数时,编译器才会选择缩小候选函数,但如果因变化可转换性而有新的拓宽候选函数,编译器将转为选择拓宽候选函数。如果因变化可转换性出现了另一个新的缩小候选函数,编译器将发出错误。

扩展方法

使用扩展方法可以向现有类型添加方法,而不必创建新的派生类型,也不必重新编译或修改原始类型。在 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 是另一个转换示例,如果在 Visual Studio 2008 中设置 Option Strict On,则会导致编译时错误,但在支持变化转换的 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)。注意:如果存在 U->T 变化转换,我们认为 T->U 为缩小转换,如果 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 类库中,这些接口的声明如下:

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 托管语言团队从事测试的软件设计工程师。在 VS 2008 版本中,他负责许多语言功能方面的工作,其中包括 LINQ 查询和词法闭包。对于即将发布的 Visual Studio 版本,他负责协变和逆变功能。您可以通过 binyamk@microsoft.com 与他联系。

衷心感谢以下技术专家对本文的审阅: Beth Massi、Lucian Wischik