🔗 协变与逆变:描述具有父 / 子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父 / 子型别关系的用语。
协变(covariant):保持了子类型序关系,该序关系是:子类型 ≤ 基类型;
逆变(contravariant):逆转了子类型序关系。
什么是协变与逆变
上面说的啥说实话我没有看懂,看看别人咋解释的:
变型(variance)是针对父子类型来说的,说人话就是:
协变(covariant):子类型的实例,能够赋值给父类型的变量;子类型可以默认转换为父类型(换句话说,父类型约束可以兼容子类型);
逆变(contravariant):协变的相反方向,一般是协变不被允许的(除了函数的参数)(换句话说,子类型约束可以兼容父类型)。
其中需要说明的是:
子类型包含所有父类型的属性,且包含额外更多的属性;
子类型的实例可以赋值给父类型的实例。
绝大多数的语言是允许协变的,也就是子类型默认可以转成父类型,即子类型实例可以赋值给父类型实例。
举个 🌰
单个变量的父子类型我们好分辨,也好理解,但是函数类型的子类型就不太好理解了,我们看个例子,来找找函数的子类型。
一些标识:
A ≥ B:表示
A
是B
的父类型(Supertyping),B
是A
的子类型(Subtyping);A → B:表示「入参类型为
A
,返回值类型为B
」的函数类型;
现约定如下类型:
定义三个类型
Animal
、Dog
、Greyhound
(灰狗);类型关系:
Animal
≥Dog
≥Greyhound
,即:Animal
是Dog
的父类型,Dog
是Greyhound
的父类型;
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
interface Greyhound extends Dog {
runFast(): void;
}
假设有一个函数 F()
接着,我们假设一个函数,我们设为函数 doSomethingInside
:
type F = (dog: Dog) => Dog;
function doSomethingInside(func: F) {
// 声明了一个 dog 实例
const dogInstance: Dog = {
name: 'dog',
bark: () => console.log('wang')
};
// 对入参的函数进行调用
// 将 dogInstance 传进去,拿到一个 dog 类型的返回值
const somethingLikeDog = func(dogInstance)
// 使用返回结果中的 bark 接口
somethingLikeDog.bark()
}
// 把下面的函数传给 `doSomethingInside()`
let f: F = (dog: Dog) => {
// 对传进来的 dog 的 bark 接口进行调用
dog.bark();
const anotherDog: Dog = {
name: "dog",
bark: () => console.log('miao'),
}
// 返回一只奇怪的 Dog
return anotherDog
}
doSomethingInside(f)
这个函数
doSomethingInside
接收一个「参数」,该参数是一个「函数」,期望的函数类型是Dog
→Dog
;将入参函数设为函数
f
,也就是说函数f
约束并期望接收一个Dog
的入参,也期望返回一个Dog
。
后来出现一个函数 G()
然后假设我们有新的函数,我们设为函数 G
:
这个函数的「入参」可能为
Animal
或者Greyhound
,「返回值」也可能为Animal
或者Greyhound
;所以
G
会有四种组合(2 种入参类型 × 2 种返回类型)。
// 组合一
type G1 = (animal: Animal) => Animal;
const g1: G1 = (animal: Animal) => ({ name: "animal" });
// 组合二
type G2 = (animal: Animal) => Greyhound;
const g2: G2 = (animal: Animal) => ({ name: "greyhound", bark: () => {}, runFast: () => {} });
// 组合三
type G3 = (greyhound: Greyhound) => Animal;
const g3: G3 = (greyhound: Greyhound) => ({ name: "animal" });
// 组合四
type G4 = (greyhound: Greyhound) => Greyhound;
const g4: G4 = (greyhound: Greyhound) => ({ name: "greyhound", bark: () => {}, runFast: () => {} });
函数 G() 要取代函数 F()
最后,我们来讨论一下这 4 个「新函数 G
」,与「目标函数类型 F
」的兼容性,我们要找到函数 F()
类型的子类型;
我们先来看看如果在编辑器中,TSLint 会报什么错误:
type F = (dog: Dog) => Dog;
let f: F = (dog: Dog) => ({ name: "dog", bark: () => {} });
/* (animal: Animal) => Animal */
f = g1; // ❌ Error 返回值类型不匹配
// 不能将类型 “G1” 分配给类型 “F”。
// 类型 "Animal" 中缺少属性 "bark",但类型 "Dog" 中需要该属性。ts(2322)
/* (animal: Animal) => Greyhound */
f = g2; // ✅ Correct
/* (greyhound: Greyhound) => Animal */
f = g3; // ❌ Error 参数类型和返回值类型均不匹配
// 不能将类型 “G3” 分配给类型 “F”。
// 参数 “greyhound” 和 “dog” 的类型不兼容。
// 类型 "Dog" 中缺少属性 "runFast",但类型 "Greyhound" 中需要该属性。ts(2322)
/* (greyhound: Greyhound) => Greyhound */
f = g4; // ❌ Error 参数类型不匹配
// 不能将类型 “G4” 分配给类型 “F”。
// 参数 “greyhound” 和 “dog” 的类型不兼容。
// 不能将类型 “Dog” 分配给类型 “Greyhound”。ts(2322)
上面代码中,我们要用函数 g()
去取代函数 f()
,函数 g()
内部和函数 f()
内部完全不一样;
在赋值过程中的报错原因:
因为函数 doSomethingInside
会传一个 Dog
类型的变量给函数 g()
,对于函数 g()
而言,函数 g()
需要能够处理 Dog
类型的变量;
所以 g()
的入参类型只能是 Animal
,不能是 Greyhound
,因为传入的参数 Dog
类型中不会有 runFast()
接口。
因为 doSomethingInside
要求函数 g()
要返回一个 Dog
类型的变量,即函数 g()
的返回值中要由 bark()
接口;
所以 g
的返回指类型只能是 Greyhound
,不能是 Animal
,因为返回 Animal
的话,返回值中没有 bark()
接口。
综上所述,函数 f()
只能被入参类型是 Animal
,返回值类型是 Greyhound
的函数类型,即 g2()
。
总结一下子
根据提示,回想最开始的,我们总结一下:
传入的参数必须包含足够多的数据,函数内部才能处理原本传入的参数类型,即可以传入子类型变量,给父类型的入参,故参数的类型是「逆变」的。
返回的数据必须包含足够多的数据,函数外部才能处理函数返回的返回值,即子类型可以,故返回值的类型是「协变」的。
所以我们知道了,从父子类型的角度来看,Dog
→ Dog
的子类型是 Animal
→ Greyhound
,其中函数的参数是「逆变」的。
回到最初:绝大多数的语言是允许协变的,同时逆变一般是不允许的,除了函数的入参。