hello2dj

if you can't explain it simply, you don't understand it well enough

简话协变和逆变

原文地址

一图胜千言

什么是协变和逆变?

子类型(subtyping)在编程语言理论中一直是个复杂的话题。对协变和逆变的误解是造成这个问题的一个主要原因。这篇文章就是来说明这两个术语的。

接下来我们将会使用以下符号:

  • A <: B 意思是A是B的子类型
  • A -> B 代表一个函数参数是A,返回值是B
  • e : T 意思是e的类型是T

一个有意思的问题

假设我们有这么三个类型

1
Greyhoud <: Dog <: Animal

可以看出Greyhound 是Dog的子类型,并且Dog 是Animal的子类型。通常来说,子类型是具有传递性的,因此Greyhound也是Animal的子类型。

问题: 下面那个类型是Dog -> Dog的子类型

  1. Greyhound -> Greyhound
  2. Greyhound -> Animal
  3. Animal -> Animal
  4. Animal -> Greyhound

我们如何回答这个问题呢?假设f是一个接收Dog -> Dog 函数类型作为参数的函数。此时我们并不关心f的返回值类型,举个栗子吧,假设:

1
f: (Dog -> Dog) -> String

现在我想用g作为参数来调用f,接下来我们一一看一下当g是上述类型时会是什么情况:

  1. 假设g: Grehound -> Grehound, 那个f(g)是否是类型安全的呢?
    不是的,因为f有可能会用其他的Dog的子类型来调用g,比如:GemanShepherd

  2. 假设g: Geryhound -> Animal, 那个f(g)是否是类型安全的呢?
    不是的,原因同上

  3. 假设g: Animal -> Animal, 那个f(g)是否是类型安全的呢?
    不是的,因为f有可能调用了g,然后使用它的返回值让他吠,但并不是所有的动物都会吠。

  4. 假设g: Animal -> Greyhound, 那个f(g)是否是类型安全的呢?
    是的,这个是安全的,f可以使用任意类型的Dog调用g,因为所有的Dog都是Animal,并且,f可以假设g的返回值就是Dog,因为所有的Greyhound都是Dog。

那么接下来该说啥呢?

可以看出这个是类型安全的:

1
(Animal -> Greyhound) <: (Dog -> Dog)

返回值的类型很直接可以看出: Greyhound 是Dog的子类型。但是参数的类型有点儿炸: Animal是Dog的祖(super)类型啊!

为了用我们的行话(jargon)来解释这个奇怪的原因,我们规定参数的返回值类型是协变的,然而参数类型是逆变的。返回值是协变的意味着:A <: B暗指(T -> A) <: (T -> B)(A是B的子类就是说(T -> A)是 (T -> B)的子类型)。参数类型的逆变意味着: A <: B 暗指(B -> T)<: (A -T)(A和B交换位置)

有趣的事实是:在Typescript中,参数类型是双变的(即可以是逆变又可以是协变),很显然这是不合理(unsound)的表现(但是在2.6中可以使用–strictFunctionTypes来修正)

那么其他类型呢?

问题: 那么List是List的子类型么?

这问题的答案不是那么好说明的?如果list是不可变的,那么他就是类型安全的,但如果list是可变的,那么他就肯定不是安全的!

为什么呢?假设我需要一个List然后你给我传了一个List,然而我认为你给我传的就是List,那么我就有可能往list中再插入一个Cat, 那么你的List里面就有了一个Cat!显然类型系统是不允许你这么做的。

正式说明: 当我们的list是不可变(数据是否可变)的时候我们是允许类型是可协变的,但是若是可变(数据是否可变)得list那么list类型必须是不可变的(是指类型是否可变,无论是协变还是逆变都是不可以的)。

有趣的事实: 在Java里面,数组即是可变(是指数据是否可变)的又是类型可协变。显然这是不合理的(unsound)