高级前端
ts
【Q692】什么是协变与逆变

什么是协变与逆变

Issue 欢迎在 Gtihub Issue 中回答此问题: Issue 713 (opens in a new tab)

Author 回答者: shfshanyue (opens in a new tab)

协变与逆变(Covariance and contravariance )是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

Author 回答者: Carrie999 (opens in a new tab)

https://github.com/sl1673495/blogs/issues/54 (opens in a new tab)

Author 回答者: Carrie999 (opens in a new tab)

https://www.zhihu.com/question/38861374 (opens in a new tab)

Author 回答者: huangyulie (opens in a new tab)

TypeScript 逆变和协变

文章中所有的代码均在这里 TS Playground - An online editor for exploring TypeScript and JavaScript (opens in a new tab)

一些概念

Subtyping 子类型

在编程语言理论中,子类型是一种类型多态的形式,这种类型可以替换其超类型(supertype)。如果 S 是 T 的子类型,一般表示为 S <: T,意思是在任何类型为 T 的地方都可以安全的使用 S。

type T = number
type S = T | string
 
type Fn = (arg: T) => void
 
let fn: Fn = (arg: S) => {}

上面的代码我们定义了 T 和 S 类型,然后定义了一个 参数为 T 返回值为 any 的函数类型 Fn, 但是在具体的赋值的时候我们将 args 变为 S,程序是可以正常运行的。

协变与逆变

协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。 在一门程序设计语言的类型系统 (opens in a new tab)中,一个类型规则或者类型构造器是:

  • 协变(covariant),如果它保持了子类型序关系≦ (opens in a new tab)。该序关系是:子类型≦基类型。
  • 逆变(contravariant),如果它逆转了子类型序关系。
  • 不变(invariant),如果上述两种均不适用。 维基百科中的描述有点抽象,我们提取一下关键词
  • 父/子型别
  • 构造器构造
  • 是否具有父/子型别关系 我们先用简单的数学模拟一下,假设我们现在有两个值 x = -1 , y = -5,x 和 y 的关系为 x > y。 假设现在有函数 $$f(t) = t + 10$$ 将我们的 x 和 y 代入 f(t) 后得到 f(x) = 9 和 f(y) = 5 ,可以发现 f(x) 和 f(y) 的关系仍然为 f(x) > f(y),这就是典型的协变。 假设如下函数 $$g(t) = t^2$$ g(x) = 1 < g(y) = 5 这就是逆变。 上面只是举了一个很简单并且不够严谨的例子,主要是为了方便理解,现在我们看一下在 ts 中的实现。

TypeScript实现

考虑如下类型

dinterface Animal {
    name: string
}
 
interface Dog extends Animal {
    // 摇尾巴
    waggingTail(): void
}
 
interface Corgi extends Dog {
    canWagging: false
}

代码中我们定义了三个类型,他们之间的子集关系为 Corgi <: Dog <: Animal。

// 协变
type AnimalList = Array<Animal>
type DogList = Array<Dog>
 
let animalArr: AnimalList = [];
let dogArr: DogList = [];
// ✅ DogList 为 AnimalList 子集
animalArr = dogArr;

我们再考虑一种特殊情况

type Fn1 = (arg: Dog) => Dog

Fn1 类型的子类型是什么呢? 是 (arg: Corgi) => Corgi吗? 考虑如下场景 image 显然 Fn3 并不是 Fn1 的子类型。 我们分析一下这样为什么不被允许,由于 Fn3 类型的限制,这里 fn3 函数的参数只能传入 arg 为 Corgi 类型的函数,但是本来的 Fn1 是允许任何 Dog 类型的参数,所以 ts 禁止了这样的操作。 所以 Fn1 的子类型应该为参数可以接受任何类型的 Dog,返回值具有 Dog 所有值的类型。 所以正确答案是 (arg: Animal) => Corgi 。 image

为了方便表示,这里我们定义 A → B 指的是以 A 为参数类型,以 B 为返回值类型的函数类型。 返回值类型很容易理解:柯基是狗的子类型。但参数类型则是相反的:动物是狗的父类。 用合适的术语来描述这个奇怪的表现,可以说我们允许一个函数类型中,返回值类型是协变的,而参数类型是逆变的。返回值类型是协变的,意思是 A <: B 就意味着 (T → A) <: (T → B) 。参数类型是逆变的,意思是 A <: B 就意味着 (B → T) <: (A → T) ( A 和 B 的位置颠倒过来了)。 一个有趣的现象:在 TypeScript 中, 参数类型是双向协变的 (opens in a new tab) ,也就是说既是协变又是逆变的,而这并不安全。但是现在你可以在 TypeScript 2.6 (opens in a new tab) 版本中通过 --strictFunctionTypes 或 --strict 标记来修复这个问题。

参考

Author 回答者: pkc918 (opens in a new tab)

dinterface Animal -> interface