Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【Q692】什么是协变与逆变 #713

Open
shfshanyue opened this issue Aug 9, 2021 · 5 comments
Open

【Q692】什么是协变与逆变 #713

shfshanyue opened this issue Aug 9, 2021 · 5 comments
Labels

Comments

@shfshanyue
Copy link
Owner

No description provided.

@shfshanyue shfshanyue added the ts label Aug 9, 2021
@shfshanyue
Copy link
Owner Author

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

@Carrie999
Copy link

sl1673495/blogs#54

@Carrie999
Copy link

@huangyulie
Copy link

TypeScript 逆变和协变

文章中所有的代码均在这里 TS Playground - An online editor for exploring TypeScript and JavaScript

一些概念

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)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
在一门程序设计语言的类型系统中,一个类型规则或者类型构造器是:

  • 协变(covariant),如果它保持了子类型序关系≦。该序关系是:子类型≦基类型。
  • 逆变(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 中, 参数类型是双向协变的 ,也就是说既是协变又是逆变的,而这并不安全。但是现在你可以在 TypeScript 2.6 版本中通过 --strictFunctionTypes 或 --strict 标记来修复这个问题。

参考

@pkc918
Copy link

pkc918 commented Aug 10, 2023

dinterface Animal -> interface

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants