为了方便,约定A → B
指的是以 A
为参数类型,以 B
为返回值类型的函数类型。
我们先不谈论逆变与协变。看一下一个有趣的问题。
Greyhound
(灰狗)是Dog
(狗)的子类,而Dog
则是Animal
(动物)的子类。由于子类型通常是可传递的,因此我们也称Greyhound
是Animal
的子类。问题:以下哪种类型是
Dog → Dog
的子类呢?
Greyhound → Greyhound
Greyhound → Animal
Animal → Animal
Animal → Greyhound
以代码来演示,先定义类 。
class Animal {
public speak() {
console.log("Animal speak")
}
}
class Dog extends Animal {
public eatBone() {
console.log("eat bone")
}
}
class Grayhaund extends Dog {
public catchRabbit(){
console.log("catch Rabbit")
}
}
class GermanShepherd extends Dog {
}
type TargetFunction = (dog: Dog) => Dog;
let fun: (g: TargetFunction) => any;
fun
是一个以 Dog → Dog
为参数的函数 。 上面fun
函数并没有完整定义,下面将重点谈论fun
的内容。
我们知道,g被传入fun,那它很可能被调用,此时g要求输入一个参数
fun = function(g){
const dog = g(new Dog());
const grayhaund = g(new Grayhaund());
const germanShepherd = g(new GermanShepherd());
//报错
//类型“Animal”的参数不能赋给类型“Dog”的参数。
const animal = g(new Animal())
}
我们看到, g的参数类型被限制为Dog
及其子类。
接着,我们看返回类型
fun = function(g){
const dog = g(new Dog());
const dog2 = g(new Grayhaund());
dog.speak();
dog.eatBone();
dog2.speak();
dog2.eatBone();
//报错
//Dog上没有方法catchRabbit
dog.catchRabbit();
dog2.catchRabbit();
}
强调一点,g是未知的,g的所有返回值类型都被推断为Dog
类型,与g的内容无关。
所以, g的返回值只能使用Dog
及其超类(Animal)上的方法。
所以,fun定义的函数内容通过编译的必要条件:
- g的参数类型被限制为
Dog
及其子类。 - g的返回值只能使用
Dog
及其超类(Animal)上的方法。
回到开头,哪种会通过编译?
f(g)
//下面哪种不使f(g)报错?
//1. Greyhound → Greyhound
//2. Greyhound → Animal
//3. Animal → Animal
//4 .Animal → Greyhound
如果你认真看上面的分析,很快就能得出答案,第四种可以通过编译。
前提 fun 内容可以通过编译 则
参数类型一定是Dog
及其子类,那么它也一定满足继承于Dog的超类(Animal
)。
返回值只能使用Dog
及其超类(Animal)上的方法,那么它是Dog
的子类一定可以调用这些方法。
此时,我们称 返回值类型是协变的,而参数类型是逆变的。
A ≼ B
意味着A
是B
的子类型。返回值类型是协变的,意思是
A ≼ B
就意味着(T → A) ≼ (T → B)
。参数类型是逆变的,意思是
A ≼ B
就意味着(B → T) ≼ (A → T)
其他类型
这是王垠博客提出的一个问题。
typescript中也有这个问题
let a:string[] = [];
//这么赋值并不报错
let b:Obeject[] = a;
//下面并不报错
b[0] = "123";
b[1] = 1;
(有个基础知识你需要知道,所有的基本类型都是在被包装到Object的原型链上的,可以用Object原型的任何方法,所以Object类型可以赋任何基础值。)
但是,这可能是typescript有意而为之。沿用上面示例的类
let a:Animal[] = [];
//b报错
//不能将类型“Animal[]”分配给类型“Dog[]”。
// Property 'foo' is missing in type 'Animal' but required in type 'Dog'.ts(2322)
let b:Dog[] = a;
let c:Dog[] = [];
let d:Animal[] = c;
我们看到如果是按照逆变来赋值,是会报错的,说明typescript是检查这种情况的。
如果不报错,考虑下面这种情况。
let a:Animal[] = [];
let b:Dog[] = a;
a[0] = new Animal();
b[0].eatBone();
Duck类型没有eatBone
方法 这就是unsafe了。
不过顺变也有这个问题
let c:Dog[] = [];
let d:Animal[] = c;
d[0] = new Animal();
c[0].eatBone();
Animal
类上没有eatBone
, 这也是 unsafe的。
该如何解决?
我们看一位大佬的解答(他的知乎)
它这个的设计错误在于,数组的索引器是一个输入输出的接口,可以看做一对getter setter方法。
setter方法接收的参数类型应该是逆变的。
这里a虽然能协变到b,但是a[]的参数类型a到b[]的参数类型a不是逆变关系
他的意思是,如果按上例赋值,b[0]
在赋值时,应该限制在Dog
类及其子类内,这样才能保证一定有方法调用。
不学typescript啦类型系统太复杂头发都掉光了哭唧唧。