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); // 类型安全
发布评论
评论(3)
首先“协变”是啥这个题主清楚吗?
如果你不清楚,我先简单介绍一下,想详细了解可以自己搜索一下。
这个名词虽然不是 OOP 里才有的,但一般现在我们讲它都是在 OOP 的语境下。因为 OOP 里有继承这种东西存在。
那么假设有
class Student extends Person
存在,也就是Person p = new Student()
成立(我们把这个成立条件称作条件 V,用于下面代称)。那么如何决定Student
和Person
二者的更复杂类型之间的关系,叫做“变型”。一般有以下三种情况(以数组为例):
Student[]
同时也是Person[]
,即Person[] arr = new Student[]
成立,与条件 V 的兼容性一致。Person[]
同时也是Student[]
,即Student[] arr = new Person[]
成立,与条件 V 的兼容性相反。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
关闭函数的双向协变,只保留逆变。楼上说了背景知识
我补充个具体的例子吧
所以:F(Troop) => LavaHound 对于 F(AirTroop) => AirTroop是类型安全的
可以看到对于返回类型 LavaHound是AirTroop的子类,而参数类型则相反,Troop是AirTroop的父类。也就是所谓的返回类型是协变,而参数类型是逆变
TypeScript的
Bivariance
就是指函数参数两样都行,这明显是不安全的,比如对应代码f(g1)的情况。是
完成上述定义之后,如此使用
doIt()
doIt()
的定义,第 2 个参数应该消费一个Person
所以doIt()
内部可能会给它一个不是Student
的Person
。但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 有所不同)!