如何理解ts的函数参数双向协变?

发布于 2022-09-13 01:20:53 字数 190 浏览 15 评论 0

image.png文档地址https://www.tslang.cn/docs/ha...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

凝望流年 2022-09-20 01:20:53

首先“协变”是啥这个题主清楚吗?

如果你不清楚,我先简单介绍一下,想详细了解可以自己搜索一下。

这个名词虽然不是 OOP 里才有的,但一般现在我们讲它都是在 OOP 的语境下。因为 OOP 里有继承这种东西存在。

那么假设有 class Student extends Person 存在,也就是 Person p = new Student() 成立(我们把这个成立条件称作条件 V,用于下面代称)。那么如何决定 StudentPerson 二者的更复杂类型之间的关系,叫做“变型”。

一般有以下三种情况(以数组为例):

  1. 协变(Covariant):Student[] 同时也是 Person[],即 Person[] arr = new Student[] 成立,与条件 V 的兼容性一致。
  2. 逆变(Contravariant),Person[] 同时也是 Student[],即 Student[] arr = new Person[] 成立,与条件 V 的兼容性相反。
  3. 不变(Invariant),既不是协变也不是逆变,即 Student[]Person[] 没有关系。除非你强转,否则你不能直接把一个类型的变量赋值给另一个类型的变量。

简记就是:协变多变少,逆变少变多(子类型是父类型的扩展,所以理论上子类型的属性比父类型要多)。

上面是以数组为例,还有很多变体的存在,比如泛型(TS 里数组就是泛型的,但大部分 OOP 语言不是这样)Foo<Student>Foo<Parent> 是否可以互转;再比如函数 (e: Student) => void(e: Parent) => void 是否可以互转;等等等等。

那什么叫“双向协变(Bivariant)”呢?其实就是协变 + 逆变都成立。


回到问题里,为什么说 TS 的函数是双向协变的。

因为 TS 是鸭子类型系统,只要两个对象结构一致,就认为是同一种类型,而不需要两者的实际类型有显式的继承关系。

因为这个特点,所以 TS 里有很多一般 OOP 语言中没有的特性,函数的双向协变就是其中之一。

图中的代码已经给了一个例子:MouseEvent extends Event,在函数中 type f1 = (e: MouseEvent) => void 可以与 type f2 = (e: Event) => void 互换。

P.S. 可以通过编译选项 --strictFunctionTypes true 关闭函数的双向协变,只保留逆变。

若能看破又如何 2022-09-20 01:20:53

楼上说了背景知识
我补充个具体的例子吧

class Troop {};
class AirTroop {
    attackFromTheSky(){}
}
class LavaHound extends AirTroop {
    aggro(){}
}
class Balloon extends AirTroop{
    avalanche() {}
}
type AttackStrategy = (arg:AirTroop) => AirTroop;

var f = (g:AttackStrategy) => {
    var ball = new Balloon();
    ball.avalanche();
    var n = g(ball);
    n.attackFromTheSky();
}

var g1 = (n:LavaHound) => n;
f(g1);  //类型不安全,因为f会以ballon为参数调用g

var g2 = (n:Troop) => n;
f(g2); //类型不安全,因为f会使用g的返回值,不是所有的部队都能从空中进攻

var g3 = (n: Troop) => new LavaHound();
f(g3); // 类型安全

所以:F(Troop) => LavaHound 对于 F(AirTroop) => AirTroop是类型安全的
可以看到对于返回类型 LavaHound是AirTroop的子类,而参数类型则相反,Troop是AirTroop的父类。也就是所谓的返回类型是协变,而参数类型是逆变

TypeScript的Bivariance就是指函数参数两样都行,这明显是不安全的,比如对应代码f(g1)的情况。

冰之心 2022-09-20 01:20:53

interface Person {
    name: string;
}

interface Student extends Person {
    grade: number;
}

function doIt(
    provider: () => Person,
    customer: (v: Person) => void,
): void {
    const p = provider();
    customer(p);
}

const personProvider = () => ({ name: "James" } as Person);
const studentProvider = () => ({ name: "Jack", grade: 7 } as Student);

const personCustomer = (p: Person) => console.log(p.name);
const studentCustomer = (p: Student) => console.log(`${p.name} in ${p.grade}`);

完成上述定义之后,如此使用 doIt()

doIt(personProvider, personCustomer);
doIt(personProvider, studentCustomer);
//                   ^^^^^^^^^^^^^^^ ①
doIt(studentProvider, personCustomer);
//   ^^^^^^^^^^^^^^^ ②
doIt(studentProvider, studentCustomer);
//                    ^^^^^^^^^^^^^^^ ③
  • ① 根据 doIt() 的定义,第 2 个参数应该消费一个 Person 所以 doIt() 内部可能会给它一个不是 StudentPerson。但 studentCustomer 需要消费一个 Student,很显然 doIt() 给它消费的东西不符合要求 —— 如果给的只是一个 Person,那 studentCustomer 中使用 p.grade 时就会不找到 grade 属性。
  • doIt() 需要提供者提供一个 Person,然后它内部会在 Person 声明的有限属性/方法中进行操作。所以只要提供者提供的是一个 Person(其子类对象也是 Person)就行,而 Student 确实是 Person,所以 studentProvider 可用。

    子类对象一定是父类类型,但父类对象不一定是子类类型。想像一下:学生是人,但人不一定是学生。

  • ③ 虽然提供者确实提供了一个 Student,但是 doIt() 是把它当作 Person 看待的(根据声明),它并不清楚这是一个可以给 studnetCustomer 消费的对象。当然也不需要知道,按 ① 的原理,只需要直接判 studentCustomer 不匹配类型即可。

这里 doIt() 的第一个参数是“产出”型,只要它输出的类型符合 doIt() 需要的类型(或其子类)即可,这就是协变。doIt() 的第二个参数是“输入”型,doIt() 需要提供一个符合其要求类型的对象,也就是说 doIt() 提供的对象是其要求的类型或者子类型,反过来说,它需要的输入的类型只能是 doIt() 声明可提供类型的父类型,不能是子类型,这就是逆变。

简单的理解:

协变就是我声明需要一个类型,你提供了这个类型或其子类型,就很 OK。这符合 Parent p(声明的) = s as Sub (提供的) 的常识,称为“协”。

逆变就是,我声明提供一个类型,你可以消费这种类型,就

没问题。因为传入参数(比如回调)中声明的类型必须是我声明可提供的类型的父类型,看起来像是 Sub s (声明的) = p as Parent (提供的),所以称为“逆”。

但其实不管是协还是逆,都可以抛开声明方和提供方,直接从数据流向来考虑。从数据流向来说,都是 子 ⇒ 父,符合类型约束常识。

至于 TypeScript 的“双向协变”,看下图,事实证明它不允许(也许是 4.x 对这方面的类型安全性进行了增强,会与 3.x 有所不同)!

image.png

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文