逆变和协变
$$
\dfrac{T_1 \leq S_1 \quad S_2 \leq T_2}{S_1 \rightarrow S_2 \leq T_1 \rightarrow T_2}
$$
简单的代换
假设有三个类,Animal ,Cat,Dog。其中 Cat 和 Dog 是 Animal 的子类。
有一段这样的转换代码
1 | Animal a = new Cat(); |
有哪些可以通过编译呢?
显然对面向对象有基本了解到人都能很快答出只有第一行能编译通过。
第二行因为不是所有 Animal 都是 Cat 。所以在赋值的时候转换失败。
第三行因为 Dog 和 Cat 是不同的类,除非定义转换方式或者内存分布一致,否则通常是失败的。
第一行可以编译通过是因为面向对象的继承特性。
继承允许子类拥有父类相同的行为,又可以有自己独特的行为。
所以子类替换父类,也就是著名的里氏代换原则。
复合类型
除了基本的类型比如 int ,bool 等,还有一种复合类型比如 int[],List< int > 等。
复合类型通常由类型构造器和类型参数组合而成,类似与函数和参数的关系。
比如 List< int > 的类型构造器是 List ,参数为 int
简单类型的替换非常直观,子类能够替换父类,父类不能替换子类。
复合类型的替换就不是那么直观了。
比如对于类型 Animal[] Animal数组和Cat[] Cat数组,这两个类型。
这个两个类型是什么关系?是否与 Animal 和 Cat 的关系有关?
答案是这两个类型没有任何关系。
仔细想想就能发现
Cat[] 并不能替换所有 Animal[] 。因为 Animal[] 可以装入 Dog 的实例,而 Cat[] 不可以。
Animal[] 也不能替换所有 Cat[] 。因为 Cat[] 中的 Cat 可以调用 Cat 类独有的方法,替换为 Animal 后,Animal 没有这个独有的方法,调用失败。
所以 Animal[] 和 Cat[] 是没有关系的。
是不是所有的 Animal 的复合类型和 Cat 的复合类型都没有关系呢?
其实不是的。
对于 Reader (->)
有
(Animal -> Cat) 是 (Animal -> Animal) 的子类型
(Animal -> Animal) 是 (Cat -> Animal) 的子类型
先说比较直观的第一个
要想说明 (Animal -> Cat) 是 (Animal -> Animal) 的子类型,需要子类能够替换父类。
显然父类 (Animal -> Animal) 的返回值是 Animal ,如果替换成子类 (Animal -> Cat)
输入类型 Animal 不变没问题,输出变成了Cat 。因为 Cat 能够替换 Animal 所以是没问题的。
所以 (Animal -> Cat) 可以替换 (Animal -> Animal)
所以 (Animal -> Cat) 是 (Animal -> Animal) 的子类型。
再看第二个
如果 (Animal -> Animal) 替换 (Cat -> Animal)。
输出都是 Animal 没问题。
输入变成了 Animal。看起来似乎是有问题的,因为 (Cat -> Animal) 中有可能调用 Cat 的独有方法
其实是没问题的,因为替换成 (Animal -> Animal) 后,(Animal -> Animal) 没有 Cat 的独有方法,所以不会报错。
而且原本的输入 Cat 是可以转成现在的输入 Animal 的。
所以 (Animal -> Animal) 可以替换 (Cat -> Animal) 。
所以 (Animal -> Animal) 是 (Cat -> Animal) 的子类型。
(Animal -> Cat) 替换 (Animal -> Animal) 中用 Cat 替换 Animal 与原本的继承关系 Cat 替换 Animal 一致,所以称为协变。
(Animal -> Animal) 替换 (Cat -> Animal) 中用 Animal 替换 Cat 与原本的继承关系 Cat 替换 Animal 相反,所以称为逆变。
继承
C++ 和 Java 允许通过继承父类重载函数实现返回值的协变,以 C++ 为例
1 | class Animal {}; |
参数化多态
C# 支持在参数化多态中使用 in
和 out
标记逆变和协变
1 | class OutExample |
不对,走错片场了。是这个
1 | public class GrandParent { } |