协变和逆变

很多语言的类型系统都有子类型:

  • C++ 允许 继承,但是并不是单根的对象结构
  • Java 和 C# 允许继承,默认从一个类似 Object 的对象开始继承
  • Rust 的生命周期有子关系

实际上这些说了跟白说一样,哈哈哈,那我们认真来说说:

Variance

类型有子类型的关系,但是这些关系涉及 容器、数组 这些东西的时候,事情肯定不太一样:

  • Cat is an Animal

比如可以从 wiki 中找到:

  • IEnumerable<Cat>IEnumerable<Animal>的子类型,因为类型构造器IEnumerable<T>是协变的(covariant)。注意到复杂类型IEnumerable的子类型关系和其接口中的参数类型是一致的,亦即,参数类型之间的子类型关系被保持住了。

  • Action<Cat>Action<Animal>的超类型,因为类型构造器Action<T>是逆变的(contravariant)。(在此,Action<T>被用来表示一个参数类型为Tsub-T一级函数)。注意到T的子类型关系在复杂类型Action的封装下是反转的,但是当它被视为函数的参数时其子类型关系是被保持的。

  • IList<Cat>IList<Animal>彼此之间没有子类型关系。因为IList<T>类型构造器是不变的(invariant),所以参数类型之间的子类型关系被忽略了。

所以,可以找到形式化的定义:

  • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
  • 逆变(contravariant),如果它逆转了子类型序关系。
  • 不变(invariant),如果上述两种均不适用。

数组

首先考虑数组类型构造器: 从Animal类型,可以得到Animal[](“animal数组”)。 是否可以把它当作

  • 协变:一个Cat[]也是一个Animal[]
  • 逆变:一个Animal[]也是一个Cat[]
  • 以上二者均不是(不变)?

如果要避免类型错误,且数组支持对其元素的读、写操作,那么只有第3个选择是安全的。Animal[]并不是总能当作Cat[],因为当一个客户读取数组并期望得到一个Cat,但Animal[]中包含的可能是个Dog。所以逆变规则是不安全的。

反之,一个Cat[]也不能被当作一个Animal[]。因为总是可以把一个Dog放到Animal[]中。在协变数组,这就不能保证是安全的,因为背后的存储可以实际是Cat[]。因此协变规则也不是安全的—数组构造器应该是不变。注意,这仅是可写(mutable)数组的问题;对于不可写(只读)数组,协变规则是安全的。

这示例了一般现像。只读数据类型(源)是协变的;只写数据类型(汇/sink)是逆变的。可读可写类型应是“不变”的。

在 Java 里面,对于你用到的容器,可以有 bound 来维持一定的信息。

在旧的版本中,你需要把T[]数组转成 object[] 来处理

OOP 的 variance

返回值协变

Animal:

Animal getMyself() {
return new Animal();
}

在 Java 中,允许你返回一个 Cat 并说明自己是继承来的:

@Override
Cat getMyself() {
return new Cat();
}

但是 C# 似乎不行。

参数类型

https://stackoverflow.com/questions/5007357/java-generics-bridge-method?answertab=votes#tab-top

可以看到,Java 有类似 Bridge Method 的方法。用安全的方式完成这种变换。

Generic & Variance

在 C# 中,你可以很基础的:

struct RefSample<T> where T: class {...}
struct ValSample<T> where T: struct {...}

同时,C# 有转换类型约束(需要指定的不能是密封类):

where T: IComparable<T>

Java 用 擦除处理 Generic;

C# 4 提供了受限的可变性, 用 in/out 指定对应的协变性和逆变性,使编译器允许这种情况发生。

Rust

子类型?

如果'big: 'small(big包含small,或者big比small长寿),那么'big就是'small的子类型。这一点很容易弄错,因为它和我们的直觉是相反的:大的范围是小的范围的子类型。不过如果你对比一下我们举的Animal的例子就清楚了:Cat是一个Animal外加一些独有的东西,而'big'small外加一些独有的东西。

所以 Rust 的变性是针对 生命周期 来定义的:

一些重要的变性(下文会详细描述):

  • &'a T对于'aT是协变的
  • &'a mut T对于'a是协变的,对于T是不变的
  • fn(T) -> U对于T是逆变的,对于U是协变的
  • BoxVec以及所有的集合类对于它们保存的类型都是协变的
  • UnsafeCell<T>Cell<T>,RefCell<T>,Mutex<T>和其他的内部可变类型对于T都是不变的