协变和逆变是什么?
我们先来看看维基百科的解释:
协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
简单的说,协变和逆变是一种关系
如何解释协变与逆变
背景
我们先假设有以下这几种类型以及关系:
Animal
>= Dog
>= Greyhound(灰狗)
以上的类型规定,很显然 灰狗属于狗,狗属于动物,那我们有以下的几种函数的类型
(Animal) → Animal
(Dog) → Dog
(Greyhound) → Greyhound
假设现在有一个函数g
,他的参数为(Dog) → Dog
,即:
1 | function g(animal: Animal, dogCallback: (Dog)=> Dog): string{ |
我们要调用函数g
,在第二个参数dogCallback
的位置传入一个函数,我们暂且叫它f
提出问题
我们要传入何种类型的f
,才能使得函数g
接收的参数dogCallback
的类型是安全的呢?
思考问题
我们假设使用(Greyhound) → Greyhound
类型,记为f:(Greyhound) → Greyhound
从上面f
的返回值类型上看,Greyhound
是属于Dog
,返回值类型是安全,因为灰狗是狗的子类型,我可以调用返回值上是Dog
的所有公共属性和方法,没有什么问题。
我们看看参数类型部分是否安全:
g
的参数上定义第二个参数dogCallback
g
传入的参数dog
的类型是Dog
,涵盖所有狗的类型
传入的 f
函数的参数只被定义为 Greyhound
类型
在函数g
中调用f
时,Greyhound
类型的参数不能接收Dog
类型的变量,传入变量dog
的值可能是Greyhound
之外的类型,例如 BlackDog
所以,在参数中,Dog
到 Greyhound
不安全,因为Dog
的值可能是狗,但不是灰狗,可能是其他类型的狗
解决问题
那传入什么类型的函数才是安全的呢?答案是(不止以下两种):
(Dog) → Greyhound
(Animal) → Greyhound
在参数上,Animal
或者 Dog
可以将 Dog
的所有子类型包含在内,因此这是安全的
理解概念
返回值的类型要是返回类型,或其子类型,例如 返回类型定为Dog
,返回值类型为Dog
的子类型Greyhound
,返回类型安全
参数类型 必须是 传入的参数值的类型,或其的子类型,例如参数要求的类型为Dog
,传入的参数必须要包含Dog
的所有子类型,例如Animal
。 Animal
包含了Dog
,所以Animal
到 Dog
是安全的。
但是传入的子类型不能为 Greyhound
, 因为Greyhound
到Dog
不安全,即Dog
类型可能会将其他非Greyhound
的类型传进去
这样就意味着(Animal → Greyhound) ≼ (Dog → Dog)
, 也就是(Dog → Dog)
是(Animal → Greyhound)
的子类型
比较好理解是从Dog
到Greyhound
,由多到少,很好理解,我们可以称这种变体为协变
Animal
到 Dog
,这个变体过程由多变少, 我们称这个变体过程为逆变
总结
从宏观上来理解:
协变(covariant):类型收敛
逆变(contravariant):类型发散
在 TypeScript 中:
对象、类、数组和函数的返回值类型都是协变关系,而函数的参数类型是逆变关系
返回值类型是 协变 的,意味着如果
A
为B
的子类型,则(T) =>A
为(T) => B
的子类型参数类型是 逆变 的,意味着如果
A
为B
的子类型, 则(B)=> T
为(A) => T
的子类型