C# 中的协变和逆变
一、可变性的概念
C# 一开始就支持数组的协变,听说是为了和 Java 竞争于是就把 Java 的这个不怎么样的特性给实现了。
1 | object[] myArray = new string[] { "abc", "def", "ghi", // ... }; |
所谓协变性指的就是,在一个使用宽泛类型的地方,可以传入一个具体类型的对象。那么,最基本的协变就是面向对象中的多态——使用基类型对象的地方,都可以传入派生类对象。
但是,上面这个数组的协变性还是很特别的。首先,在 C# 的类型系统中,派生类型的数组并不继承自基类型的数组(别的语言也是如此吧)。所以,它的确是一种新的协变性。其次,如果你做一个如下操作,你就会收到运行时错误,告诉你数组类型不匹配:
1 | myArray[0] = 3; |
也就是说,CLR 还是知道 myArray
到底是什么类型的,并且不许改变。
逆变就是反过来的概念——在一个使用具体类型的地方,可以传入宽泛类型的对象。
二、委托中的可变性
在 C# 1 中,如果我们定义一个委托类型,那么用于它的方法将必须在参数表和返回值方面严格匹配。但是 C# 2 里事情改变了,它支持对参数的逆变性和对返回值的协变性。
假定
1 | class Base {} |
那么下面的代码是合法的
1 | delegate Base VariantDelegate(Derived d); |
在委托的标准中,需要Base类型的返回值,Derived类型的参数,而我们传入的MyFunc是Derived类型的返回值,Base类型的参数,这样是合理的
考虑使用这个委托的地方,会调用委托的实例d(new Derived())
,而MyFunc本身需要一个Base类型的参数,不会有问题;委托的实例d(new Derived).(From Base…)
应用返回值一直是按Base来处理,所以MyFunc返回Derived也没有任何问题。
这其实就是消费代码必须把传入对象当做是一个更加泛化(宽泛)的对象来处理。
三、泛型中的可变性
一直到 C# 3,泛型类型、接口、委托的参数都是不可变的。基于和上述类似的逻辑,C# 4 终于决定在泛型接口和泛型委托中支持类型参数的可变性。如果你想使用可变性,必须在类型参数前用 in
或者 out
修饰符来显式指定。和前面类似,如果一个类型参数仅用作接口方法或者委托中的(普通)参数,那么它可以被指定为逆变的(使用 in
来修饰);如果它只作为返回值,那么它可以被指定为协变的(使用 out
来修饰)。
1 | delegate void Action<in T>(T t); |
如果理解了上面介绍的关于(非泛型)委托参数和返回值的可变性,那么对这样的泛型委托也可以很容易的理解。但是,如果情况复杂了怎么办呢?考虑下面这个情况:
1 | delegate void MyAction<T>(Action<T> action); |
这个委托如果要受可变性的恩泽,应该在 T
前面加什么修饰符呢?答案是 out
,而书上的解释是模糊的:“作为一个便捷的规则,可以认为内嵌的逆变性反转了之前的可变性。”
1 | using System; |
引用的《C# in depth》上的解释:object
本身是比 string
更泛化的类型,但逆变性使得 Action<string>
成了比 Action<object>
更泛化的类型。
1 | using System; |
可变类型参数的使用还具有以下限制
- 仅可在泛型接口和泛型委托的声明中使用
- 仅适用于引用类型,如果使用值类型填充类型参数,则得到的封闭类型是不可变的
- 可变性不能用于委托链,即不能组合两个封闭类型不完全一致的委托
1 | //编译时错误 |
四、内置的可变类型
.NET类库中有很多泛型接口或委托都使用了可变的类型参数,下面列举一些这类接口和委托
具有协变类型参数的泛型接口
1 | public interface IEnumerable<out T> : IEnumerable |
具有逆变类型参数的泛型接口
1 | public interface IComparer<in T> |
具有协变类型参数的委托类型
1 | public delegate TResult Func<out TResult>(); |
具有逆变类型参数的委托类型
1 | public delegate void Action<in T>(T obj); |
同时具有协变类型参数和逆变类型参数的委托类型
1 | public delegate TResult Func<in T, out TResult>(T arg); |