泛型中的协变和逆变

协变逆变 是指使用比最初指定的更派生类型(更具体)或更少的派生类型(不太具体)的功能的术语。 泛型类型参数支持协变和逆变,以便更好地分配和使用泛型类型。

当您在谈论类型系统时,协变、逆变和不变性具有以下定义。 这些示例假定一个名为Base的基类和一个名为Derived的派生类。

  • Covariance

    使你能够使用比最初指定的更具体的派生类型。

    可以将IEnumerable<Derived>的实例分配给类型为IEnumerable<Base>的变量。

  • Contravariance

    使你能够使用比最初指定的更泛型(更少的派生)类型。

    可以将Action<Base>的实例分配给类型为Action<Derived>的变量。

  • Invariance

    这意味着只能使用最初指定的类型。 固定泛型类型参数既不是协变,也不是逆变。

    不能将List<Base>的实例分配给List<Derived>类型的变量,反之亦然。

利用协变类型参数,你可以执行非常类似于普通的多态性的分配,如以下代码中所示。

IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;
Dim d As IEnumerable(Of Derived) = New List(Of Derived)
Dim b As IEnumerable(Of Base) = d

List<T> 实现 IEnumerable<T> 接口,因此 List<Derived>List(Of Derived) 在 Visual Basic 中)实现 IEnumerable<Derived>。 协变类型参数处理剩余的工作。

另一方面,逆变似乎适得其反。 以下示例创建类型 Action<Base>Action(Of Base) 在 Visual Basic 中)的委托,然后将该委托分配给类型的 Action<Derived>变量。

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());
Dim b As Action(Of Base) = Sub(target As Base)
                               Console.WriteLine(target.GetType().Name)
                           End Sub
Dim d As Action(Of Derived) = b
d(New Derived())

这看起来有些反常,但它是类型安全、可编译并能运行的代码。 lambda 表达式与其分配到的委托匹配,因此它定义了一个方法,该方法接受一个类型为 Base 的参数且没有返回值。 可以将结果委托分配给类型类型 Action<Derived> 的变量,因为 T 委托的类型参数 Action<T> 是逆变类型参数。 代码是类型安全的,因为 T 指定参数类型。 当类型为 Action<Base> 的委托被调用时,就像它是类型为 Action<Derived> 的委托一样,其参数必须是类型 Derived。 此参数始终可以安全地传递给基础方法,因为该方法的参数的类型 Base

通常,协变类型参数可用作委托的返回类型,逆变类型参数可用作参数类型。 对于接口,协变类型参数可用作接口方法的返回类型,逆变类型参数可用作接口方法的参数类型。

协变和逆变统称为 方差。 未标记为协变或逆变的泛型类型参数称为 固定。 有关公共语言运行时差异事实的简要摘要:

  • 变体类型参数仅限于泛型接口和泛型委托类型。

  • 泛型接口或泛型委托类型可以同时具有协变和逆变类型参数。

  • 差异仅适用于引用类型;如果为变体类型参数指定值类型,则类型参数对于生成的构造类型是固定的。

  • 变体不适用于委托组合。 也就是说,给定两个类型为Action<Derived>Action<Base>的委托(在 Visual Basic 中为Action(Of Derived)Action(Of Base)),你不能将第二个委托与第一个委托组合在一起,尽管这样做在类型上是安全的。 变体允许将第二个委托分配给具有Action<Derived>类型的变量,但委托只有在类型完全匹配时才能合并。

  • 从 C# 9 开始,支持协变返回类型。 重写方法可以声明比它重写的方法派生程度更高的返回类型,而重写的只读属性可以声明派生程度更高的类型。

具有协变类型参数的泛型接口

多个泛型接口具有协变类型参数,例如、IEnumerable<T>IEnumerator<T>IQueryable<T>IGrouping<TKey,TElement>。 这些接口的所有类型参数都是协变的,因此类型参数仅用于成员的返回类型。

下面的示例演示协变类型参数。 该示例定义了两种类型:Base 具有一个静态方法,方法名为 PrintBases,它接受 IEnumerable<Base>(在 Visual Basic 中为 IEnumerable(Of Base))并且打印元素。 Derived 继承自 Base. 该示例创建了一个空的 List<Derived>(在 Visual Basic 中为 List(Of Derived)),并演示此类型如何能够在不进行强制转换的情况下传递给 PrintBases 并分配给类型为 IEnumerable<Base> 的变量。 List<T> 实现了 IEnumerable<T>IEnumerable<T> 具有一个协变类型参数。 协变类型参数是可以使用实例 IEnumerable<Derived> 而不是使用 IEnumerable<Base>协变类型参数的原因。

using System;
using System.Collections.Generic;

class Base
{
    public static void PrintBases(IEnumerable<Base> bases)
    {
        foreach(Base b in bases)
        {
            Console.WriteLine(b);
        }
    }
}

class Derived : Base
{
    public static void Main()
    {
        List<Derived> dlist = new List<Derived>();

        Derived.PrintBases(dlist);
        IEnumerable<Base> bIEnum = dlist;
    }
}
Imports System.Collections.Generic

Class Base
    Public Shared Sub PrintBases(ByVal bases As IEnumerable(Of Base))
        For Each b As Base In bases
            Console.WriteLine(b)
        Next
    End Sub
End Class

Class Derived
    Inherits Base

    Shared Sub Main()
        Dim dlist As New List(Of Derived)()

        Derived.PrintBases(dlist)
        Dim bIEnum As IEnumerable(Of Base) = dlist
    End Sub
End Class

具有逆变类型参数的泛型接口

多个泛型接口具有逆变类型参数;例如: IComparer<T>IComparable<T>IEqualityComparer<T>。 这些接口只有逆变类型参数,因此类型参数仅用作接口成员中的参数类型。

以下示例演示逆变类型参数。 该示例定义具有 MustInherit 属性的抽象(在 Visual Basic 中为 ShapeArea 类。 该示例还定义一个实现 ShapeAreaComparer(在 Visual Basic 中为 IComparer<Shape>)的 IComparer(Of Shape) 类。 IComparer<T>.Compare 方法的实现基于 Area 属性的值,因此可以使用 ShapeAreaComparer 按区域对 Shape 对象进行排序。

Circle 类继承 Shape 并替代 Area。 该示例使用一个采用 SortedSet<T>(在 Visual Basic 中为 Circle)的构造函数创建 IComparer<Circle> 对象的 IComparer(Of Circle)。 但是,该示例没有传递IComparer<Circle>,而是传递一个实现ShapeAreaComparerIComparer<Shape>对象。 当代码调用派生程度较高的类型 (Shape) 的比较器时,该示例可以传递派生程度较低的类型 (Circle) 的比较器,因为 IComparer<T> 泛型接口的类型参数是逆变参数。

将新的Circle对象添加到SortedSet<Circle>时,每次将新元素与现有元素进行比较时,都会调用IComparer<Shape>.Compare对象的IComparer(Of Shape).Compare方法(在 Visual Basic 中称为ShapeAreaComparer方法)。 方法的参数类型(Shape)派生量小于传递的类型(Circle),因此调用的类型是安全的。 逆变允许 ShapeAreaComparer 对任何单一类型的集合以及派生自 Shape的类型混合集合进行排序。

using System;
using System.Collections.Generic;

abstract class Shape
{
    public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
    private double r;
    public Circle(double radius) { r = radius; }
    public double Radius { get { return r; }}
    public override double Area { get { return Math.PI * r * r; }}
}

class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
    int IComparer<Shape>.Compare(Shape a, Shape b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.Area.CompareTo(b.Area);
    }
}

class Program
{
    static void Main()
    {
        // You can pass ShapeAreaComparer, which implements IComparer<Shape>,
        // even though the constructor for SortedSet<Circle> expects
        // IComparer<Circle>, because type parameter T of IComparer<T> is
        // contravariant.
        SortedSet<Circle> circlesByArea =
            new SortedSet<Circle>(new ShapeAreaComparer())
                { new Circle(7.2), new Circle(100), null, new Circle(.01) };

        foreach (Circle c in circlesByArea)
        {
            Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
        }
    }
}

/* This code example produces the following output:

null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
 */
Imports System.Collections.Generic

MustInherit Class Shape
    Public MustOverride ReadOnly Property Area As Double
End Class

Class Circle
    Inherits Shape

    Private r As Double
    Public Sub New(ByVal radius As Double)
        r = radius
    End Sub
    Public ReadOnly Property Radius As Double
        Get
            Return r
        End Get
    End Property
    Public Overrides ReadOnly Property Area As Double
        Get
            Return Math.Pi * r * r
        End Get
    End Property
End Class

Class ShapeAreaComparer
    Implements System.Collections.Generic.IComparer(Of Shape)

    Private Function AreaComparer(ByVal a As Shape, ByVal b As Shape) As Integer _
            Implements System.Collections.Generic.IComparer(Of Shape).Compare
        If a Is Nothing Then Return If(b Is Nothing, 0, -1)
        Return If(b Is Nothing, 1, a.Area.CompareTo(b.Area))
    End Function
End Class

Class Program
    Shared Sub Main()
        ' You can pass ShapeAreaComparer, which implements IComparer(Of Shape),
        ' even though the constructor for SortedSet(Of Circle) expects 
        ' IComparer(Of Circle), because type parameter T of IComparer(Of T)
        ' is contravariant.
        Dim circlesByArea As New SortedSet(Of Circle)(New ShapeAreaComparer()) _
            From {New Circle(7.2), New Circle(100), Nothing, New Circle(.01)}

        For Each c As Circle In circlesByArea
            Console.WriteLine(If(c Is Nothing, "Nothing", "Circle with area " & c.Area))
        Next
    End Sub
End Class

' This code example produces the following output:
'
'Nothing
'Circle with area 0.000314159265358979
'Circle with area 162.860163162095
'Circle with area 31415.9265358979

具有 Variant 类型参数的泛型委托

Func泛型委托(例如Func<T,TResult>)具有协变返回类型和逆变参数类型。 Action泛型委托(例如Action<T1,T2>)具有逆变参数类型。 这意味着可以将委托分配给具有更多派生参数类型的变量,并且(在泛型委托的情况下 Func )派生的返回类型较少。

注释

泛型委托的最后一个泛型类型参数 Func 指定委托签名中的返回值的类型。 它是协变(out 关键字),而其他泛型类型参数是逆变(in 关键字)。

以下代码演示了这一点。 第一段代码定义了一个名为Base的类,一个继承Derived的类名为Base,以及另一个具有static方法(在Visual Basic中为Shared)名为MyMethod的类。 该方法接收一个 Base 实例并返回一个 Derived 实例。 (如果参数是Derived的一个实例,MyMethod返回该参数;如果参数是Base的实例,MyMethod则返回Derived的一个新实例。)在Main()示例中,示例创建了一个Func<Base, Derived>(在 Visual Basic 中为Func(Of Base, Derived))的实例,该实例表示MyMethod,并将其存储到变量f1中。

public class Base {}
public class Derived : Base {}

public class Program
{
    public static Derived MyMethod(Base b)
    {
        return b as Derived ?? new Derived();
    }

    static void Main()
    {
        Func<Base, Derived> f1 = MyMethod;
Public Class Base
End Class
Public Class Derived
    Inherits Base
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal b As Base) As Derived
        Return If(TypeOf b Is Derived, b, New Derived())
    End Function

    Shared Sub Main()
        Dim f1 As Func(Of Base, Derived) = AddressOf MyMethod

第二段代码显示委托可以分配给类型 Func<Base, Base>Func(Of Base, Base) 在 Visual Basic 中)的变量,因为返回类型为协变。

// Covariant return type.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());
' Covariant return type.
Dim f2 As Func(Of Base, Base) = f1
Dim b2 As Base = f2(New Base())

第三段代码显示可以将委托分配给类型 Func<Derived, Derived>Func(Of Derived, Derived) 在 Visual Basic 中)的变量,因为参数类型是逆变的。

// Contravariant parameter type.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());
' Contravariant parameter type.
Dim f3 As Func(Of Derived, Derived) = f1
Dim d3 As Derived = f3(New Derived())

最后一段代码显示可以将委托分配给类型 Func<Derived, Base>Func(Of Derived, Base) 在 Visual Basic 中)的变量,并结合逆变参数类型和协变返回类型的效果。

// Covariant return type and contravariant parameter type.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
' Covariant return type and contravariant parameter type.
Dim f4 As Func(Of Derived, Base) = f1
Dim b4 As Base = f4(New Derived())

非泛型委托中的变体

在前面的代码中,MyMethod 的签名与构造的泛型委托的签名完全匹配:Func<Base, Derived> (在 Visual Basic 中为 Func(Of Base, Derived))。 该示例显示,只要所有委托类型都是从泛型委托类型 Func<T,TResult> 构造的,此泛型委托可以存储在那些具有派生程度较高的参数类型和派生程度较低的返回类型的变量或方法参数中。

这是一个重要点。 泛型委托的类型参数中协变和逆变的影响类似于普通委托绑定中的协变和逆变的影响(请参阅委托中的方差(C#)委托中的方差(Visual Basic)。 但是,委托绑定中的变体适用于所有委托类型,而不仅仅是具有变体类型参数的泛型委托类型。 此外,委托绑定中的方差使方法能够绑定到任何具有更限制性参数类型和限制性较低的返回类型的委托,而泛型委托的分配仅在两个委托类型都是基于同一泛型类型定义构造的时才起作用的。

以下示例演示委托绑定中的方差和泛型类型参数中的方差的组合效果。 该示例定义一个类型层次结构,其中包括三种类型,从最不派生的 (Type1) 到最派生的 (Type3)。 普通委托绑定中的变化用于将参数类型为 Type1 、返回类型为 Type3 的方法绑定到参数类型为 Type2 、返回类型为 Type2的泛型委托。 然后,生成的泛型委托被分配给另一个变量,该变量的泛型委托类型具有类型Type3的参数和返回类型Type1。这利用了泛型类型参数的协变和逆变。 第二个赋值要求从同一泛型类型定义(在本例 Func<T,TResult>中)构造变量类型和委托类型。

using System;

public class Type1 {}
public class Type2 : Type1 {}
public class Type3 : Type2 {}

public class Program
{
    public static Type3 MyMethod(Type1 t)
    {
        return t as Type3 ?? new Type3();
    }

    static void Main()
    {
        Func<Type2, Type2> f1 = MyMethod;

        // Covariant return type and contravariant parameter type.
        Func<Type3, Type1> f2 = f1;
        Type1 t1 = f2(new Type3());
    }
}
Public Class Type1
End Class
Public Class Type2
    Inherits Type1
End Class
Public Class Type3
    Inherits Type2
End Class

Public Class Program
    Public Shared Function MyMethod(ByVal t As Type1) As Type3
        Return If(TypeOf t Is Type3, t, New Type3())
    End Function

    Shared Sub Main()
        Dim f1 As Func(Of Type2, Type2) = AddressOf MyMethod

        ' Covariant return type and contravariant parameter type.
        Dim f2 As Func(Of Type3, Type1) = f1
        Dim t1 As Type1 = f2(New Type3())
    End Sub
End Class

定义变体泛型接口和委托

Visual Basic 和 C# 具有关键字,可用于将接口和委托的泛型类型参数标记为协变或逆变。

协变类型参数用 out 关键字(Visual Basic 中的关键字Out )标记。 可以使用协变类型参数作为属于接口的方法的返回值,也可以用作委托的返回类型。 不能将协变类型参数用作接口方法的泛型类型约束。

注释

如果接口的方法具有泛型委托类型的参数,则接口类型的协变类型参数可用于指定委托类型的逆变类型参数。

逆变类型参数用in关键字标记(在 Visual Basic 中为In关键字)。 可以使用逆变类型参数作为属于接口的方法的参数的类型,也可以用作委托的参数的类型。 可以将逆变类型参数用作接口方法的泛型类型约束。

只有接口类型和委托类型可以具有变体类型参数。 接口或委托类型可以同时具有协变和逆变类型参数。

Visual Basic 和 C# 不允许违反使用协变和逆变类型参数的规则,或向接口和委托以外的类型类型参数添加协变和逆变注释。

有关信息和示例代码,请参阅 泛型接口(C#)中的变体泛型接口(Visual Basic)中的变体

类型列表

以下接口和委托类型具有协变和/或逆变类型参数。

类型 协变类型参数 逆变类型参数
Action<T>Action<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16> 是的
Comparison<T> 是的
Converter<TInput,TOutput> 是的 是的
Func<TResult> 是的
Func<T,TResult>Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult> 是的 是的
IComparable<T> 是的
Predicate<T> 是的
IComparer<T> 是的
IEnumerable<T> 是的
IEnumerator<T> 是的
IEqualityComparer<T> 是的
IGrouping<TKey,TElement> 是的
IOrderedEnumerable<TElement> 是的
IOrderedQueryable<T> 是的
IQueryable<T> 是的

另请参阅