什么是协变?

假设我们有两个类型,AnimalDogDogAnimal的子类,我们定义以下两个集合:

List<Animal>
List<Dog>

由于DogAnimal的子类,所以Dog可以隐式转换为Animal,但C#中,List<Dog>无法隐式转换为List<Animal>List<Dog>List<Animal>没有任何继承关系,他们是两个完全独立的对象,我们称之为是不可变的。

Animal animal = new Dog(); //合法
List<Animal> animals = new List<Dog>(); //编译错误
//Cannot convert initializer type 'List<Dog>' to target type 'List<Animal>'
//Type 'Dog' doesn't match expected type 'Animal'.

为什么会有这种限制?因为这种转换看似很合理,却存在着严重的安全问题,举个例子:我们假设允许List<Dog>List<Animal>的转换,顺便我们将这种“派生类的集合能够隐式转换为基类的集合”称之为是协变的,就会出现以下情况:

//在C#中,数组是协变的,这里用数组代替List<T>演示协变性
//由于string可以隐式转换为object,所以数组的协变性允许string数组隐式转换为object数组
object[] array = new string[] {"C#", "C++"};
//而对于一个object数组,以下操作在编译期是合法的,可以通过编译
array[0] = new Animal();
//但运行时会抛出异常:System.ArrayTypeMismatchException: 
//Attempted to access an element as a type incompatible with the array.
//因为往一个string数组添加一个Animal对象显然是不合法的
//而数组的协变性导致我们无法在编译期检查出此类错误,只有运行时抛异常了才会发现问题

所以对于List<T>来讲,不支持协变可以保证类型安全,不会出现数组的协变性所导致的这种安全问题。那为什么C#的数组要设计成协变的?stackoverflow上有人提到一个解释是:

Eric Lippert says:
Unfortunately, this particular kind of covariance is broken. It was added to the CLR because Java requires it and the CLR designers wanted to be able to support Java-like languages. We then up and added it to C# because it was in the CLR. This decision was quite controversial at the time and I am not very happy about it, but there’s nothing we can do about it now.

意思就是,数组的协变性被添加到了CLR里是因为Java需要它,而CLR的设计者希望可以支持Java-like的语言,而把它添加到C#里则是因为CLR支持了这个特性,这个决定在当时是很有争议的,而且我个人不是很赞同这种做法,不过现在我们已经什么也做不了了。
那Java又为什么要把数组设计成协变的呢?答案是Java早期没有泛型,而为了实现“泛型”的需求,就允许了任何类型的数组都可以转换成object类型的数组,结果就是这样了。总而言之,数组协变是一个历史遗留缺陷了,很显然现在回过头来看,数组协变是一个很危险的设计,所以我们在使用数组时一定要注意类型安全的问题,一定要搞清楚所使用的数组的真实类型。

Tags: