协变与抗变
1. 背景
一个简单的例子,
public class Sharp
{
}
public class Rectange : Sharp
{
}
上面定义了两个简单的类,一个是图形类,一个是矩形类;它们之间有简单的继承关系:矩形是图形的一种。
接下来是常见的一种里氏替换写法:
Sharp sharp = new Rectange();
“子类引用可以直接转化成父类引用”,即 Rectange 类和 Sharp 类之间存在一种安全的隐式转换。
既然Rectange类和Sharp类之间存在一种安全的隐式转换,那数组Rectange[]和Sharp[]之间是否也存在这种安全的隐式转换呢?
这种将原本类型上存在的类型转换映射到他们的数组类型(或接口、委托等其它类型)上的能力,可称为“可变性(Variance)”。
在.NET中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换,就是上面例子的写法。
再看下面这种写法:
Sharp[] sharps=new Rectange[3];
编译通过,这说明 Rectange[] 和 Sharp[] 之间存在安全的隐式转换。
2. 定义
2.1 协变定义
在具有可变性的条件下,与原始类型转换方向相同的可变性称作协变(covariant)。
2.2 抗变定义
在具有可变性的条件下,与原始类型转换方向相反的可变性称作抗变(contravariant),也称逆变。
可变性远远不只是针对映射到数组的能力,也有映射其它集合的能力如List.
很多人会问,说了这么多,到底这个协变或者抗变有什么实际价值?
举个例子,在.net 4.0之前可以这么写:
Sharp sharp = new Rectange();
但是却不能这么写:
IEnumerable<Sharp> sharps = new List<Rectange>();
4.0之后,可以允许按上面的写法了,因为泛型接口 IEnumerable<T> 被声明成如下:
public interface IEnumerable<out T> : IEnumerable
为什么接口参数类型前加了个 out 关键字就可以安全转换了?
因为在接口类型参数前加上修饰关键字 in 和 out 可以表示该类型参数支持抗变和协变,CLR 会自动在安全范围内进行转换。
目前 C# 支持协变和抗变的有两种类型:泛型接口 和 泛型委托。
由于它俩机制差不多,这里仅记录一下泛型接口的协变和抗变。
3. 泛型接口中的协变和抗变
定义一个泛型接口:
public interface ICovariant<T>
{
}
两个类各自继承一下该接口:
public class Sharp : ICovariant<Sharp>
{
}
public class Rectange : Sharp, ICovariant<Rectange>
{
}
测试代码:
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange(