前言
协变(covariance)和 逆变(contravariance)是我们在学习泛型委托和泛型接口中遇到的比较抽象的一组概念。它们的使用方法,其实是非常简单的,即:
用
in
标记的逆变量只能出现在泛型接口的方法的输入参数中,或者作为泛型委托的输入参数;
用out
标记的协变量只能作为泛型接口方法或者泛型委托的输出参数。
但是,为什么需要将变量标记为逆变或协变,即它们产生的原理,以及工作原理,却是不太好理解。这里我们就探究一下。
本质与原因
简单本质
开门见山地说吧,协变和逆变的本质很简单,就是实现 将一个 泛型类型变量 转换为 泛型实参不同的 同一种泛型类型变量。文字表述不好理解,举个栗子:假设我定义了一个泛型委托 MyDelegate<T>
。那么,将T标记为协变或者逆变,就可以实现 将一个 MyDelegate<T1>
变量转换为MyDelegate<T2>
变量,其中T1
和T2
是两个泛型的实际参数。
当然,T1
和T2
之间需要满足一定的关联:
- 协变量:泛型类型参数可以从一个类更改为它的某个基类。例如下面的代码是合法的:
public delegate void MyDelegate<out T>();
MyDelegate<BaseClass> d_base;
MyDelegate<DerivedClass> d_son = new MyDelegate<DerivedClass>(DoSomething);
d_base = d_son;
- 逆变量:泛型类型参数可以从一个类更改为它的某个派生类。例如下面的代码是合法的:
public delegate void MyDelegate<in T>();
MyDelegate<BaseClass> d_base = new MyDelegate<BaseClass>(DoSomething);
MyDelegate<DerivedClass> d_son = new MyDelegate<DerivedClass>(DoSomething);
d_son = d_base;
这样一来,协变和逆变就很好理解了,它们本质就是 类型转换,只不过是针对泛型类型的。
产生原因
为什么泛型类型需要协变和逆变呢?我们知道,DerivedClass
是可以隐式地转换为BaseClass
的。但是当具有继承关系的类型是 泛型类型的实际参数 的时候,情况就不一样了。泛型类型会丢失泛型参数的继承关系。
泛型类型的一个特点就是,只要泛型实际参数的类型不同,CLR就会创建不同的类型对象(Type object,可参考这篇文章),把它们当做不同的类型。这也是好理解的。于是乎,即使DerivedClass
是派生自BaseClass
的,但是 IMyInterface<BaseClass>
和 IMyInterface<DerivedClass>
就是完全不同的两个类型,它们之间不再有任何继承关系。这就是问题产生的原因。
这种情况称为泛型的 不变性(invariance)。在 .NET 4 之前,尚未引入in
和out
关键字,所有泛型类型的泛型参数都是不变量(invariant),这意味着泛型类型不可更改。举一个简单案例:List<int>
与 List<long>
是不同的集合类型,不能将List<int>
转换为 List<long>
。
泛型的不变性(invariance)对我们的编码限制很大,因为有时候我们需要将泛型实参不同的同种泛型类型相互转化,于是便产生了协变和逆变。考虑这种情况:我们有一个泛型接口IMyInterface<T>
,它定义了一个方法DoSomething()
,这个方法是返回泛型实参T
的一个实例的,见下:
public interface IMyInterface<T>
{
T DoSomething();
}
public class MyImplementation<T> : IMyInterface<T>
{
public T DoSomething()
{
return (T)Activator.CreateInstance(typeof(T));
}
}
static void Main(string[] args)
{
IMyInterface<BaseClass> interface1;
IMyInterface<DerivedClass> interface2 = new MyImplementation<DerivedClass>();
interface1 = interface2;//编译失败
interface1.DoSomething();
}
在客户端调用时,我们定义了IMyInterface<BaseClass>
和IMyInterface<DerivedClass>
。由于这个接口的方法返回的是泛型实参的实例,因此我们希望能将 IMyInterface<DerivedClass>
转换为IMyInterface<BaseClass>
而 不影响 接口方法的调用,因为,前者返回的是派生类,可自动隐式转化为其父类。但是,上述Main
中的转化代码会编译失败,提示无法转换。
为了提高泛型类型的灵活性,.NET 4 引入泛型参数的 in
和 out
关键字。我们将上述 IMyInterface<T>
修改为 IMyInterface<out T>
,那么Main
中的代码就可以编译通过了。
这就是协变产生的原因,也是为什么上文中提到,协变要求泛型参数T
只能出现在输出位置的原因。
对于逆变,原因是类似的。如果泛型参数T
是作为方法的输入参数或者委托的输入参数使用,那么,一个 IMyInterface<DerivedClass>
变量传入的参数就是一个 DerivedClass
实例,那么我们希望可以将IMyInterface<DerivedClass>
变量转化为IMyInterface<BaseClass>
变量,因为后者需要 BaseClass
实例作为输入,前者的DerivedClass
实例可以自动转换为BaseClass
实例,不影响后者方法的调用。因此,逆变就被引入了。
例如,下面代码能通过编译,因为 Action 委托被定义为 Action<in T>(T obj)
:
Action<BaseClass> actionBase = MyAction;
Action<DerivedClass> actionDerived;
actionDerived = actionBase ;
广义上的协变与逆变
其实逆变和协变这两个概念与泛型是无关的。在.NET中,参数类型本来就是协变的,即:方法可以接受其参数类型的子类型实例作为实际参数;方法的返回类型是逆变的,返回结果(子类)可以被转换成其父类。
逆变可以这样理解:把一个表达式的父类形式替换成其子类的相应形式;
而协变就是:把一个表达式的子类形式替换为其父类的相应形式。
例如:
DoSomething(BaseClass)
这一个定义可以被 DoSomething(DerivedClass)
这样调用,就是协变的;
DerivedClass d = GetDerivedClass()
可以被 BaseClass b = GetDerivedClass()
这样调用,就是逆变的。
参考文献
[1] Eric Lippert 系列文章
[2] 《C#高级编程》,Christian Nagel
[3] 《CLR via C#》,第四版