协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。 泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。 在引用类型系统时,协变、逆变和不变性具有如下定义。 这些示例假定一个名为 Base
的基类和一个名为 Derived
的派生类。
Covariance
使你能够使用比原始指定的类型派生程度更大的类型。 你可以向 IEnumerable
类型的变量分配IEnumerable(Of Derived)
(在 Visual Basic
中为IEnumerable
)的实例。Contravariance
使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。 你可以向 Action
类型的变量分配Action(Of Base)
(在 Visual Basic
中为Action
)的实例。Invariance
这意味着,你只能使用原始指定的类型;固定泛型类型参数既不是协变类型,也不是逆变类型。你无法向 List
类型的变量分配 List(Of Base)
(在 Visual Basic
中为 List
)的实例,反之亦然。利用协变类型参数,你可以执行非常类似于普通的多态性的分配,如以下代码中所示。
IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;
List
类实现 IEnumerable
接口,因此 List
(在 Visual Basic
中为List(Of Derived)
)实现 IEnumerable
。 协变类型参数将执行其余的工作。
相反,逆变看起来却不够直观。 下面的示例创建类型 Action
(在 Visual Basic
中为Action(Of Base)
)的委托,然后将此委托分配给类型 Action
的变量。
Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());
此示例看起来是倒退了,但它是可编译和运行的类型安全代码。 由于 lambda
表达式与其自身所分配到的委托相匹配,因此它会定义一个方法,此方法采用一个类型 Base
的参数且没有返回值。 可以将结果委托分配给类型类型 Action
的变量,因为 T
委托的类型参数 Action
是逆变类型参数。 由于 T
指定了一个参数类型,因此该代码是类型安全代码。 在调用类型 Action
的委托(就像它是类型Action
的委托一样)时,其参数必须属于类型 Derived
。 始终可以将此实参安全地传递给基础方法,因为该方法的形参属于类型 Base
。
通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。 对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。
协变和逆变统称为“变体”。 未标记为协变或逆变的泛型类型参数称为“固定参数” 。 有关公共语言运行时中变体的事项的简短摘要:
.NET Framework 4
中,Variant
类型参数仅限于泛型接口和泛型委托类型。Variant
类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。Action
和 Action
(在 Visual Basic
中为Action(Of Derived)
和 Action(Of Base)
)的两个委托的情况下,无法将第二个委托与第一个委托结合起来,尽管结果将是类型安全的。 变体允许将第二个委托分配给类型Action
的变量,但只能在这两个委托的类型完全匹配的情况下对它们进行组合。以下各小节将详细介绍协变和逆变类型参数:
Variant
类型参数的泛型委托Variant
泛型接口和委托Variant
泛型接口和委托类型的列表从 .NET Framework 4
开始,某些泛型接口具有协变类型参数;例如: IEnumerable
和 IGrouping
。 由于这些接口的所有类型参数都是协变类型参数,因此这些类型参数只用于成员的返回类型。
下面的示例阐释了协变类型参数。 此示例定义了两个类型: Base
具有一个名为 PrintBases
的静态方法,该方法采用 IEnumerable
(在 Visual Basic
中为IEnumerable(Of Base)
)并输出元素。 Derived
继承自 Base
。此示例创建一个空 List
(在 Visual Basic
中为List(Of Derived)
),并且说明可以将该类型传递给 PrintBases
且在不进行强制转换的情况下将该类型分配给类型 IEnumerable
的变量。 List
实现 IEnumerable
,它具有一个协变类型参数。 协变类型参数是可使用 IEnumerable
的实例而非 IEnumerable
的原因。
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;
}
}
从 .NET Framework 4
开始,某些泛型接口具有逆变类型参数;例如: IComparer
和 IEqualityComparer
。 由于这些接口只具有逆变类型参数,因此这些类型参数只用作接口成员中的参数类型。
下面的示例阐释了逆变类型参数。 该示例定义具有MustInherit
属性的抽象(在 Visual Basic
中为 Shape
) Area
类。 该示例还定义一个实现 ShapeAreaComparer
(在 Visual Basic
中为 IComparer
)的IComparer(Of Shape)
类。 IComparer
方法的实现基于 Area
属性的值,所以 ShapeAreaComparer
可用于按区域对 Shape
对象排序。
Circle
类继承 Shape
并重写 Area
。 该示例创建 SortedSet
对象的 Circle
,使用采用 IComparer
(在 Visual Basic
中为IComparer(Of Circle)
)的构造函数。 但是,该对象不传递 IComparer
,而是传递一个用于实现 ShapeAreaComparer
的 IComparer
对象。 当代码需要派生程度较大的类型的比较器 (Shape
) 时,该示例可以传递派生程度较小的类型的比较器 (Circle
),因为 IComparer
泛型接口的类型参数是逆变参数。
向 Circle
中添加新 SortedSet
对象时,每次将新元素与现有元素进行比较时,都会调用 IComparer
对象的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,
// even though the constructor for SortedSet expects
// IComparer, because type parameter T of IComparer 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
*/
Variant
类型参数的泛型委托在 .NET Framework 4
中, Func
泛型委托(如 Func
)具有协变返回类型和逆变参数类型。 Action
泛型委托(如 Action
)具有逆变参数类型。 这意味着,可以将委托指派给具有派生程度较高的参数类型和(对于 Func
泛型委托)派生程度较低的返回类型的变量。
Func
泛型委托的最后一个泛型类型参数指定委托签名中返回值的类型。 该参数是协变的(out
关键字),而其他泛型类型参数是逆变的(in
关键字)。
下面的代码阐释这一点。 第一段代码定义了一个名为 Base
的类、一个名为 Derived
的类(此类继承 Base
)和另一个具有名为 static
的Shared
方法(在 Visual Basic
中为 MyMethod
)的类。 该方法采用 Base
的实例,并返回 Derived
的实例。 (如果参数是 Derived
的实例,则 MyMethod
将返回该实例;如果参数是 Base
的实例,则 MyMethod
将返回 Derived
的新实例。)在 Main()
中,该示例创建一个表示 Func
的 Func(Of Base, Derived)
(在 Visual Basic
中为 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;
第二段代码说明可以将委托分配给类型 Func
(在 Visual Basic
中为Func(Of Base, Base)
)的变量,因为返回类型是协变的。
// Covariant return type.
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base());
第三段代码说明可以将委托分配给类型 Func
(在 Visual Basic
中为Func(Of Derived, Derived)
)的变量,因为参数类型是逆变的。
// Contravariant parameter type.
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived());
最后一段代码说明可以将委托分配给类型 Func
(在 Visual Basic
中为Func(Of Derived, Base)
)的变量,从而将逆变参数类型和协变返回类型的作用结合起来。
// Covariant return type and contravariant parameter type.
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
在上面的代码中, MyMethod
的签名与所构造的泛型委托 Func
(在 Visual Basic
中为Func(Of Base, Derived)
)的签名完全匹配。 此示例说明,只要所有委托类型都是从泛型委托类型 Func
构造的,就可以将此泛型委托存储在具有派生程度更大的参数类型和派生程度更小的返回类型的变量或方法参数中。
这一点非常重要。 泛型委托的类型参数中的协方差和逆变的效果类似于普通委托绑定中的协方差和逆变的效果(请参阅委托中的差异 (C#) 和委托中的差异 (Visual Basic
))。 但是,委托绑定中的变化适用于所有委托类型,而不仅仅适用于具有 Variant
类型参数的泛型委托类型。 此外,通过委托绑定中的变化,可以将方法绑定到具有限制较多的参数类型和限制较少的返回类型的任何委托,而对于泛型委托的指派,只有在委托类型是基于同一个泛型类型定义构造的时才可以进行。
下面的示例演示委托绑定中的变化和泛型类型参数中的变化的组合效果。 该示例定义了一个类型层次结构,其中包含三个按派生程度从低到高排列的类型,即Type1
的派生程度最低,Type3
的派生程度最高。 普通委托绑定中的变化用于将参数类型为 Type1
、返回类型为 Type3
的方法绑定到参数类型为 Type2
、返回类型为 Type2
的泛型委托。 然后,使用泛型类型参数的协变和逆变,将得到的泛型委托指派给另一个变量,此变量的泛型委托类型的参数类型为 Type3
,返回类型为 Type1
。 第二个指派要求变量类型和委托类型是基于同一个泛型类型定义(在本例中为 Func
)构造的。
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());
}
}
Variant
泛型接口和委托从 .NET Framework 4
开始,Visual Basic
和 C#
提供了一些关键字,利用这些关键字,可以将接口和委托的泛型类型参数标记为协变或逆变。
从 .NET Framework 2.0 版开始,公共语言运行时支持泛型类型参数上的变化批注。 在 .NET Framework 4之前,定义包含这些批注的泛型类的唯一方法就是利用 Ilasm.exe(IL 汇编程序) 编译该类或在动态程序集中发出该类,从而使用
Microsoft 中间语言 (MSIL)。
协变类型参数用 out
关键字(在 Visual Basic
中为 Out
关键字,在 MSIL
汇编程序中为 +
)标记。 可以将协变类型参数用作属于接口的方法的返回值,或用作委托的返回类型。 但不能将协变类型参数用作接口方法的泛型类型约束。
如果接口的方法具有泛型委托类型的参数,则接口类型的协变类型参数可用于指定委托类型的逆变类型参数。
逆变类型参数用 in 关键字(在 Visual Basic
中为In
关键字,在 - MSIL
汇编程序 中为)标记。 可以将逆变类型参数用作属于接口的方法的参数类型,或用作委托的参数类型。 也可以将逆变类型参数用作接口方法的泛型类型约束。
只有接口类型和委托类型才能具有 Variant
类型参数。 接口或委托类型可以同时具有协变和逆变类型参数。
Visual Basic
和 C#
不允许违反协变和逆变类型参数的使用规则,也不允许将协变和逆变批注添加到接口和委托类型之外的类型参数中。 MSIL
汇编程序 不执行此类检查,但如果你尝试加载违反规则的类型,则会引发 TypeLoadException
。
Variant
泛型接口和委托类型的列表在 .NET Framework 4
中,下面的接口和委托类型具有协变和/或逆变类型参数。
类型 | 协变类型参数 | 逆变类型参数 |
---|---|---|
Action 至 Action |
是 | |
Comparison |
是 | |
Converter |
是 | 是 |
Func |
是 | |
Func 至 Func |
是 | 是 |
IComparable |
是 | |
Predicate |
是 | |
IComparer |
是 | |
IEnumerable |
是 | |
IEnumerator |
是 | |
IEqualityComparer |
是 | |
IGrouping |
是 | |
IOrderedEnumerable |
是 | |
IOrderedQueryable |
是 | |
IQueryable |
是 |