逆变 & 协变
开始文章之前我们先约定如下的标记:
A ≼ B
意味着A
是B
的子类型。A → B
指的是以A
为参数类型,以B
为返回值类型的函数类型。x : A
意味着x
的类型为A
。
一个有趣的问题
假设我有如下三种类型:
Greyhound ≼ Dog ≼ Animal
Greyhound
(灰狗)是Dog
(狗)的子类型,而Dog
则是Animal
(动物)的子类型。由于子类型通常是可传递的,因此我们也称Greyhound
是Animal
的子类型。 问题:以下哪种类型是Dog → Dog
的子类型呢?
Greyhound → Greyhound
Greyhound → Animal
Animal → Animal
Animal → Greyhound
让我们来思考一下如何解答这个问题。首先我们假设f
是一个以Dog → Dog
为参数的函数。它的返回值并不重要,为了具体描述问题,我们假设函数结构体是这样的:f : (Dog → Dog) → String
。 现在我想给函数f
传入某个函数g
来调用。我们来瞧瞧当g
为以上四种类型时,会发生什么情况。
我们假设
g : Greyhound → Greyhound
,f(g)
的类型是否安全?
不安全,因为在 f 内调用它的参数(g)
函数时,使用的参数可能是一个不同于灰狗但又是狗的子类型,例如GermanShepherd
(牧羊犬)。
我们假设
g : Greyhound → Animal
,f(g)
的类型是否安全?
不安全。理由同(1)。
我们假设
g : Animal → Animal
,f(g)
的类型是否安全?
不安全。因为f
有可能在调用完参数之后,让返回值,也就是Animal
(动物)狗叫。并非所有动物都会狗叫。
我们假设
g : Animal → Greyhound
,f(g)
的类型是否安全?
是的,它的类型是安全的。首先,f
可能会以任何狗的品种来作为参数调用,而所有的狗都是动物。其次,它可能会假设结果是一条狗,而所有的灰狗都是狗。
展开讲讲?
如上所述,我们得出结论:
(Animal → Greyhound) ≼ (Dog → Dog)
返回值类型很容易理解:灰狗是狗的子类型。但参数类型则是相反的:动物是狗的父类! 用合适的术语来描述这个奇怪的表现,可以说我们允许一个函数类型中,返回值类型是协变的,而参数类型是逆变的。返回值类型是协变的,意思是A ≼ B
就意味着(T → A) ≼ (T → B)
。参数类型是逆变的,意思是A ≼ B
就意味着(B → T) ≼ (A → T)
(A
和B
的位置颠倒过来了)。
一个有趣的现象:在 TypeScript
中, 参数类型是双向协变的
,也就是说既是协变又是逆变的,而这并不安全。但是现在你可以在 TypeScript 2.6
版本中通过 --strictFunctionTypes
或 --strict
标记来修复这个问题。
那其他类型呢?
问题:List<Dog>
能否为 List<Animal>
的子类型?
答案有点微妙。如果列表是不可变的(immutable),那么答案是肯定的,因为类型很安全。但是假如列表是可变的,那么答案绝对是否定的!
原因是,假设我需要一串 List<Animal>
而你传给我一串 List<Dog>
。由于我认为我拥有的是一串 List<Animal>
,我可能会尝试往列表插入一只 Cat
。那么你的 List<Dog>
里面就会有一只猫!类型系统不应该允许这种情况发生。
总结一下,我们可以允许不变的列表(immutable)在它的参数类型上是协变的,但是对于可变的列表(mutable),其参数类型则必须是不变的(invariant),既不是协变也不是逆变。