关于C++ TpeScript系列的泛型

目录
  • 一、模版
  • 二、泛型
  • 三、泛型递归
  • 四、默认泛型参数
  • 五、泛型重载

前言:

我在面试的时候,通常喜欢问候选人一些莫名其妙的问题。比如这样的问题,假如你是某个库的作者,你如何实现某个功能。这类问题一般没有正确的答案,主要意图是考察一下候选人对这个库有没有更深入的理解,次要意图是觉得这样挺好玩。玩归玩,但该严肃的时候也要严肃起来。有一次,我面试到一位用过TypeScript的同学,这让人眼前一亮(从我的经验看,国内偶尔有大厂会用,小厂基本没有)。随后,我问了句,你是怎么理解泛型的呢?问了之后,我就后悔了,因为我也不知道答案。但随后的答案让我没有后悔,因为候选人回了我一句,我不知道什么是泛型……

这件事对候选人的影响可大可小,但对我的影响挺大的。它致使我一定要写出一篇关于泛型的文章。但自从种下这个种子后,我就开始后悔了。因为越接触TS中的泛型,越觉得这个题材没什么好写的。一来呢,TS中的泛型犹如空气,经常使用却难以描述。二者呢,它太过宽泛,难以面面俱到。

今天的这篇文章将不同于这个系列的以往。这篇文章将从C++模版要解决的问题出发,引出TS泛型要解决的问题,并简答介绍一些稍微高级的使用场景。

一、模版

说起泛型,不得不提一下泛型的鼻祖,模版。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: () => ({}) }); // 正确

对于React生态,这里有一个比较值得阅读的泛型重载的实例,那就是connect函数,大家可以移步到它的源码以便了解更多。

整体而言,我不太喜欢这篇文章。究其原因,TS中的泛型使用广泛,因其设计初衷的原因,可玩性较差。但我对这种设计理念是支持的,首先,它能够满足我们定义类型的要求,其次,它做到了比C++模版更为简单易用。

到此这篇关于关于C++ TpeScript系列的泛型的文章就介绍到这了,更多相关TypeScript泛型内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++算法与泛型算法(algorithm、numeric)

    本文包括的算法有: 只读算法:find().count().accumulate().equal() 写算法:fill().fill_n().back_inserter().copy().copy_backward().replace().replace_copy().next_permutation().prev_permutation() 重排元素算法:sort().stable_sort().unique() 一.算法简介 大多数算法在头文件algorithm中.标准库还在头文件numer

  • C++ 泛型编程详解

    泛型编程与面向对象编程的目标相同,即使重用代码和抽象通用概念的技术更加简单.但是面向对象编程强调编程的数据方面,泛型编程强调的是独立于特定数据类型. 这一篇介绍一下 C++ 编程中与面向对象并列的另一大分支--泛型编程,这一篇主要介绍函数模板.类模板和成员模板三大部分 如有侵权,请联系删除,如有错误,欢迎大家指正,谢谢 泛型编程 模板是泛型编程的一种重要思想,STL(Standard Template Library,标准模板库)是采用模板实现的一个实例 函数模板 对比函数重载(同一作用域内函数

  • C++实现的泛型List类分享

    额,不要说我三心二意:一边在看.NET和CLR的原理.一边在看JavaScript.一边在看Java:有时看算法有时看Unity.Hibernate:有时看Hadoop有时看Redis:现在又开始看C++了. 以前觉得无论什么语言嘛,其实都差不多,核心思想基本一致.现在又不这么想了,其实语言的选择对软件的性能.可靠性.开发成本之类的关系很大,所以觉得还是要多接触一些比较核心的东西--那么自然是C++了.以前在学校学的C++完全是酱油,太水了基本没啥用,用来用去和C差不多,所以现在要自己学啦. 废

  • 自定义的Troop<T>泛型类( c++, java和c#)的实现代码

    Troop<T>是一个泛型列表操作类,适用于非高性能和非大数据量的要求.包括了:取值get,赋值set,追加append,插入insert,清除remove,进队enqueue,出队dequeue,交换swap,滚动roll,进栈push,出栈pop等日常操作. //for more information, please access http://www.one-lab.net using System; using System.Collections.Generic; using Sy

  • C++泛型编程基本概念详解

    目录 1.什么是泛型编程? 2.函数模板 (1)函数模板概念 (2)函数模板格式 (3)函数模板的原理 (4)函数模板的实例化 (5)模板参数的匹配原则 3.类模板 (1)类模板的定义格式 (2)类模板的实例化 总结 1.什么是泛型编程? 比如说,我们如何实现一个通用的交换函数呢?int型.double型.char型的交换 void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } vo

  • C++实现支持泛型的LFU详解

    首先定义LFU存储数据节点ListNode的结构, 此结构支持键K和值V的模板,为了在有序元素中实现比较(严格小于),这里需要重载小于号,如果此数据的使用频次最少,则小于结果为true,如果频次相等,轮次早的数据最小. template<typename K, typename V> struct ListNode { K key; V value; int freq; long cur; bool operator<(const ListNode &x) const { if

  • C++泛型算法的一些总结

    泛型算法的一些总结1.每个泛型算法的实现都独立于单独的容器,并且不依赖于容器存储的元素类型. 2.泛型算法从不直接添加或删除元素. 3.与容器的类型无关,只在一点上隐式地依赖元素类型:必须能够对元素做比较运算. A.需要某种遍历集合的方式:能够从一个元素向前移到下一个元素. B.必须能够知道是否到达了集合的末尾. C.必须能够对容器中的每一个元素与被查找的元素进行比较. D.需要一个类型来指示元素在容器中的位置,或者表示找不到该元素. 4.迭代器将算法和容器绑定起来.算法基于迭代器及其操作实现,

  • 关于C++ TpeScript系列的泛型

    目录 一.模版 二.泛型 三.泛型递归 四.默认泛型参数 五.泛型重载 前言: 我在面试的时候,通常喜欢问候选人一些莫名其妙的问题.比如这样的问题,假如你是某个库的作者,你如何实现某个功能.这类问题一般没有正确的答案,主要意图是考察一下候选人对这个库有没有更深入的理解,次要意图是觉得这样挺好玩.玩归玩,但该严肃的时候也要严肃起来.有一次,我面试到一位用过TypeScript的同学,这让人眼前一亮(从我的经验看,国内偶尔有大厂会用,小厂基本没有).随后,我问了句,你是怎么理解泛型的呢?问了之后,我

  • Java总结篇系列:Java泛型详解

    一. 泛型概念的提出(为什么需要泛型)? 首先,我们看下下面这段简短的代码: public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); for (int i = 0; i < list.size(); i++) { S

  • Java设计模块系列之书店管理系统单机版(一)

    书店管理系统: 项目练习目标 : 1.Java应用程序基本分析 2.培养面向对象编程的基本思想 3.Java基本设计模式综合应用 4.掌握分层和接口的基本设计 5.构建合理的Java应用程序包结构 6.综合应用JSE所学习的知识 7.在应用中合理使用集合框架 8.在应用中综合使用swing的常用组件 9.基本的表现层的实现机制 10.IO流和文件的基本操作 11.培养良好的Java编程习惯 12.培养调试Java程序的能力,培养改错的能力 项目功能需求 : 1.能进行操作用户的注册,修改基本信息

  • Java 泛型总结(三):通配符的使用

    简介 前两篇文章介绍了泛型的基本用法.类型擦除以及泛型数组.在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用. 这个系列的另外两篇文章: Java 泛型总结(一):基本用法与类型擦除 Java 泛型总结(二):泛型与数组 数组的协变 在了解通配符之前,先来了解一下数组.Java 中的数组是协变的,什么意思?看下面的例子: class Fruit {} class Apple extends Fruit {} class Jonathan extends Apple {} class

  • Java 泛型总结(二):泛型与数组

    简介 上一篇文章介绍了泛型的基本用法以及类型擦除的问题,现在来看看泛型和数组的关系.数组相比于Java 类库中的容器类是比较特殊的,主要体现在三个方面: 数组创建后大小便固定,但效率更高 数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查 数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了 那么当数组遇到泛型会怎样? 能否创建泛型数组呢?这是这篇文章的主要内容. 这个系列的另外两篇文章: Java 泛型总结(一):基本用法与类

  • Java 泛型总结(一):基本用法与类型擦除

    简介 Java 在 1.5 引入了泛型机制,泛型本质是参数化类型,也就是说变量的类型是一个参数,在使用时再指定为具体类型.泛型可以用于类.接口.方法,通过使用泛型可以使代码更简单.安全.然而 Java 中的泛型使用了类型擦除,所以只是伪泛型.这篇文章对泛型的使用以及存在的问题做个总结,主要参考自 <Java 编程思想>. 这个系列的另外两篇文章: Java 泛型总结(二):泛型与数组 Java 泛型总结(三):通配符的使用 基本用法 泛型类 如果有一个类 Holder 用于包装一个变量,这个变

  • Material Design系列之自定义Behavior支持所有View

    本文实例为大家分享了Android自定义Behavior支持所有View ,供大家参考,具体内容如下 一.实现效果图 这个右下角的FAB,动画当然可以多种多样,可以放在界面的任何地方,我们这里只举个例子.但是v7包中提供的Behavior目前只能是FloatingActionButton来用,所以今天我们实现的这个Behavior是支持所有的View的,可以用在ImageView.Button.Layout,只要是继承View的类都可以用. 二.自定义Behavior和动画的封装 我们知道Beh

  • SpringBoot系列教程JPA之基础环境搭建的方法

    JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Spring Data JPA是在 Hibernate 基础上封装的一款框架.JPA作为标准,实际上并没有说局限于某个固定的数据源,事实上mysql,mongo, solr都是ok的.接下来我们将介绍下springboot结合jpa 来实现mysql的curd以及更加复杂一点的sql支持 jpa系列教程将包含以下几块 环境搭建 基础的插入.修改.删除

  • c#基础系列之ref和out的深入理解

    扩展阅读 c#基础系列1---深入理解 值类型和引用类型 c#基础系列2---深入理解 String 引言 在上篇文章深入理解值类型和引用类型的时候,有的小伙伴就推荐说一说ref和out 关键字,昨天晚上彻夜难眠在想是否要谈一下呢,因为可谈的不是太多,也可能是我理解的不够深刻. C#有两种参数传递方式:传值和引用,传值就是变量的值,而引用则是传递的变量的地址: 本文中说的Ref和Out都是引用传递,Ref的重点是把值传给调用方法,Out则是得到调用方法的值,类似于有返回类型的方法返回的值: 在使

  • ASP.NET Core依赖注入系列教程之服务的注册与提供

    前言 在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core中的DI容器体现为一个实现了IServiceProvider接口的对象. ServiceProvider与ServiceDescriptor 服务的注册与提供     利用ServiceProvider来提供服务     提供一个服务实例的集合     获取ServiceProvider自身对

随机推荐