温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何理解C++ TpeScript系列的泛型

发布时间:2021-10-08 09:07:29 来源:亿速云 阅读:142 作者:iii 栏目:开发技术

这篇文章主要介绍“如何理解C++ TpeScript系列的泛型”,在日常操作中,相信很多人在如何理解C++ TpeScript系列的泛型问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何理解C++ TpeScript系列的泛型”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

目录
  • 一、模版

  • 二、泛型

  • 三、泛型递归

  • 四、默认泛型参数

  • 五、泛型重载

一、模版

说起泛型,不得不提一下泛型的鼻祖,模版。C++中的模版以烧脑壳和强大著称,并被各类大牛津津乐道多年。就现在而言,Java、.NET或TS中的泛型都可以被认为是实现了C++模版的子集。对于子集的说法,我不敢苟同。因为就存在的目的而言,TS和C++模版完全不一样。

C++模版的出现是为了产生类型安全的通用容器。我们先来说一下通用容器,比如我写了个链表或者数组,这个数据结构不太关心存在里面的具体数据是什么类型,它都可以实现对应的操作。但js本身不关注类型和大小,所以js中的数组本来就是通用容器。对于TS而言,泛型的出现就可以解决这个问题。另一个值得对比的是产生,C++模版最终产出的是对应的类或函数,但对于TS而言,TS无法产生任何东西。有的同学可能要问了,TS不是最终产生JS代码吗?这样说有点不严谨,因为TS最终是分离出了JS代码,而没有对原有逻辑做任何处理。

C++模版的另一个目的就是元编程。这个元编程相当地强大,它主要通过编译时的程序设计构造来优化程序的执行。就TS而言,目前它只做了一处类似的优化,就是const enum可以内联在执行的地方,仅此而已。关于这类优化,上篇结束的位置也提到了基于类型推导的优化,但目前而言,TS还没有这个功能。倘若这类简单的优化都不支持,那对于更为复杂的元编程而言,就更不可能了(元编程需要对泛型参数进行逻辑推导,并最终内联到使用到的地方)。

关于C++模版,就说这么多吧,毕竟这不是一篇关于模版元编程的文章,而且我也不是专家,更多关于模版的问题,可以去问问轮子哥。说这么多模版,主要还是想说,TS中的泛型和模版是非常不一样的!如果你是从C++Java转来做前端,仍然需要重新认识一下TS中的泛型。

二、泛型

我认为TS中的泛型主要有3个主要用途:

  • 声明泛型容器或组件。比如:各种容器类MapArray、Set等;各种组件,比如React.Component

  • 对类型进行约束。比如:使用extends约束传入参数符合某种特定结构。

  • 生成新的类型

关于第二、三点,因为之前文章已经很清楚地提到过,这里不再赘述。关于第一点,我这里举两个例子:

第一个例子是关于泛型容器,假如我想实现一个简单的泛型链表,代码如下:

class LinkedList<T> { // 泛型类
  value: T;
  next?: LinkedList<T>; // 可以使用自身进行类型声明
  constructor(value: T, next?: LinkedList<T>) {
    this.value = value;
    this.next = next;
  }
  log() {
    if (this.next) {
      this.next.log();
    }
    console.log(this.value);
  }
}
let list: LinkedList<number>; // 泛型特化为number
[1, 2, 3].forEach(value => {
  list = new LinkedList(value, list);
});
list.log(); // 1 2 3

第二个是泛型组件,假如我想实现一个通用的表单组件,可以这样写:

function Form<T extends { [key: string]: any }>({ data }: { data: T }) {
  return (
    <form>
      {data.map((value, key) => <input name={key} value={value} />)}
    </form>
  )
}

这个例子不止演示了泛型组件,也演示了如何使用extends定义泛型约束。现实中的泛型表单组件可能比这个更为复杂,上面只是演示一下思路。

到此为止,TS的泛型就讲完了!但这个文章还没完,下面我们来看一下泛型的一些高级使用技巧。

三、泛型递归

递归简单来说就是函数的输出可以继续作为输入来进行逻辑演算的一类解决问题的思路。举个简单的例子,比如我们要算加法,定义了一个add函数,它只能求两个数的和,但现在我们有1,2,3等三个数需要计算,那我们如何用现有的工具解决这个问题呢?答案很简单,首先算add(1, 2)是3,然后add(3, 3)是6。这就是递归的思路。

在现实生活中,递归是如此的常见,以至于我们经常忽略它的存在。程序的世界也是如此。这里举个例子,并用这个例子来说明TS中的递归如何实现。比如,我现在有个泛型类型ReturnType<T>,它可以返回一个函数的返回类型。但我现在有个调用层级很深的函数,而且我不知道它的层级有多深,我该如何做呢?

思路一:

type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends (
  ...args: any
) => any
  ? DeepReturnType<ReturnType<T>> // 这里引用自身
  : ReturnType<T>;

上面代码的说明:这里定义了一个DeepReturnType的泛型类型,类型约束为接受任意参数、返回任意类型的函数。若它的返回类型是个函数,则继续用返回类型调用自身,否则返回函数的返回类型。

任何直观、简洁的方案背后都有一个但是。但是,这个是无法通过编译的。主要原因是,TS暂时不支持。以后支不支持我不知道,但,官方给的理由很明确:

  • 这个有着环形的意图不可能构成对象图,除非你以某种方式推迟(通过惰性或状态)。

  • 真的没有办法知道类型推导是否结束。

  • 我们可以在编译器中使用有限类型的递归,但问题不在于类型是否终止,而是计算密集程度和内存分配律如何。

  • 一个元问题:我们是否希望人们编写这样的代码?这种使用场景是存在的,但这样实现的类型不一定适合库的消费者。

  • 结论:我们还没有为这种件事做好准备。

所以,我们该如何实现这类需求呢?方法是有的,如官方给出的思路,我们可以使用有限次数的递归。下面给出我的思路:

// 两层泛型类型
type ReturnType1<T extends (...args: any) => any> = ReturnType<T> extends (
  ...args: any
) => any
  ? ReturnType<ReturnType<T>>
  : ReturnType<T>;
// 三层泛型类型
type ReturnType2<T extends (...args: any) => any> = ReturnType<T> extends (
  ...args: any
) => any
  ? ReturnType1<ReturnType<T>>
  : ReturnType<T>;
// 四层泛型类型,可以满足绝大多数情况
type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends (
  ...args: any
) => any
  ? ReturnType2<ReturnType<T>>
  : ReturnType<T>;
  
// 测试
const deep3Fn = () => () => () => () => "flag is win" as const; // 四层函数
type Returned = DeepReturnType<typeof deep3Fn>; // type Returned = "flag is win"
const deep1Fn = () => "flag is win" as const; // 一层函数
type Returned = DeepReturnType<typeof deep1Fn>; // type Returned = "flag is win"

这种技巧可以推广到定义深层结构的ExcludeOptionalRequired等等。

四、默认泛型参数

有时候我们很喜欢泛型,但有时候我们又不希望类或函数的消费者每次都指定泛型的类型,这时候,我们可以使用默认的泛型参数。这个在很多第三方库中广泛使用,比如:

// 接收P S C的泛型组件
class Component<P,S,C> {
  props: P;
  state: S;
  context:C
  ....
}
// 需要这样使用
class MyComponent extends Component<{}, {}, {}>{}

// 但如果我的组件是个很纯粹的组件,并不需要props、state和context呢
// 可以这样定义
class Component<P = {}, S = {}, C = {}> {
  props: P;
  state: S;
  context:C
  ....
}
// 然后可以这么使用
class MyComponent extends Component {}

我觉得这个特性非常实用,它以一种js中很自然的方式实现了C++模版中的partial instantiation

五、泛型重载

泛型重载在官方文档上提过几嘴,这种重载依赖于函数重载的一些机制,因此,我们先来看一下TS中的函数重载吧。这里,我用lodash里面的map函数来举例。map函数的第二个参数可以接受一个string或是function比如官网的例子:

const square = (n) => n * n;

// 接收函数的map
map({ 'a': 4, 'b': 8 }, square);
// => [16, 64] (iteration order is not guaranteed)
 
const users = [
  { 'user': 'barney' },
  { 'user': 'fred' }
];

// 接收string的map
map(users, 'user');
// => ['barney', 'fred']

那么,这样的类型声明如何在TS中表达呢?我可以使用函数重载,比如这样:

// 这里只做演示,不保证正确性。真实场景下这里需要填充正确的类型,而不是any
interface MapFn {
  (obj: any, prop: string): any; // 当接收string时的情况,情景一
  (obj: any, fn: (value: any) => any): any; // 当接收函数时的情况,情景二
}
const map: MapFn = () => ({});

map(users, 'user'); // 重载情景一
map({ 'a': 4, 'b': 8 }, square); // 重载情景二

上面这段代码使用了TS中比较奇特的一种机制,也就是函数、new等 类函数的定义可以写在interface中。这个特性的出现主要是为了支持js中可调用的对象,比如,在jQuery中,我们可以直接执行$("#banner-message"),或者调用其方法 $.ajax()。

当然,也可以使用另一种更为传统的做法,比如下面这样:

function map(obj: any, prop: string): any;
function map(obj: any, fn: (value: any) => any): any;
function map(obj, secondary): any {}

这里,基本讲清楚了函数重载。推广到泛型,基本上是一样的。这里举一个知友提的问题的例子,对于这个问题,这里不再赘述。解决思路大概是这样的:

interface FN {
  (obj: { value: string; onChange: () => {} }): void;
  <T extends {[P in keyof T]: never}>(obj: T): void;
  //  ,对于obj的类型T而言,它始终不接收其它的key。
}

const fn: FN = () => {};

fn({}); // 正确
fn({ value: "Hi" }); // 错误
fn({ onChange: () => {} }); // 错误
fn({ value: "Hi", onChange: () => ({}) }); // 正确

到此,关于“如何理解C++ TpeScript系列的泛型”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI