泛型

状态: 初稿

介绍

考虑组件的结构合理, 接口稳定, 特别是实现可重用, 是软件工程思想一个重要组成部分.
如果一个组件既能处理今天的数据, 又能处理明天的数据, 你就能以最灵活的方式构建大型软件系统.

在 C#, Java 等语言的工具箱中, 泛型是创建可重用组件最受欢迎的工具之一, 借助泛型, 你能够创建适用于多种数据类型的通用组件.
用户可以重用这些组件以处理自己的自定义类型.

第一个程序

作为开始, 我们来编写泛型的 “hello world” 程序: identity 函数.
函数功能与 echo 命令差不多.
直接返回函数参数.

通常(没有泛型), identity 参数类型可能是 number:

1
2
3
function identity(arg: number): number {
return arg;
}

又或是 any:

1
2
3
function identity(arg: any): any {
return arg;
}

第二种情况已经有泛型的意思, 函数参数 arg 能接收所有 / 任何类型的实参, 但是, 无论实参是 number 还是别的什么类型, 函数都返回 any.
换句话说, 实参在函数内部打了个转, 我们丢失了它的类型信息.

所以我们需要一种方法记录参数的类型, 从而顺理成章地推出返回值类型.
在 TypeScript 中, 我们使用一种不记录值, 只记录类型本身的特殊变量 — 类型参数.

1
2
3
function identity<T>(arg: T): T {
return arg;
}

经过修改, 我们为 identity 函数添加了一个类型参数 T.
这个 T 将用户确定的实际类型记录下来 (例如: number), 供我们以后使用.
观察函数签名, 我们看到, T 同时作为参数 arg 的类型和函数返回值类型.
于是我们把类型信息从函数输入传递到了输出.

由于这个函数对多种数据类型通用, 我们称这是一个泛型函数.
any 不同, 如果参数和返回值类型是 number, 这个函数会如同最初的 identity 一样精准(它不丢失任何信息).

给定泛型函数 identity, 可从以下两种方式中任选其一调用.
第一种向函数传递包括类型参数在内的所有必要参数:

1
let output = identity<string>("myString");  // type of output will be 'string'

在这个例子中, 我们显式指出 Tstring, 与函数参数不同, 类型参数要用 <> 括起来.

第二种方式 — 也可能是最常见的, 类型参数推导. 下例, 我们要求编译器根据实参类型自动求出 T 值:

1
let output = identity("myString");  // type of output will be 'string'

这次我们并没有用一对尖括号初始化类型参数; 编译器只要搞清楚 "myString" 的类型(这是显而易见的), 就能推出 T 的值.
类型参数推导有助于缩短代码长度, 提高可读性. 但在更复杂的, 以至于编译器无法自动推导类型参数的情形, 你还是要按照第一种方式所做的, 显式指出类型参数.

使用类型参数

你将发现, 一旦你开始使用泛型, 并创建像 identity 那样的泛型函数, 编译器会检查你是否在函数体内正确使用每个依赖类型参数的变量.
站在你的立场, 你必须把类型参数变量当作任何 / 所有类型对待.

再看 identity 函数:

1
2
3
function identity<T>(arg: T): T {
return arg;
}

假如我们想把每次调用的实参 arg 的长度记录到日志中, 你会怎么做?
你可能会写下:

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

很遗憾, 编译器指出取得 arglength 属性那条语句有语法错误, 因为我们从未担保 arg 有一个叫 length 的成员.
我们早先说过, 类型参数是类型本身的变量, 能代表任何 / 所有类型, 函数用户就可能用 number 初始化 T, 而 number 没有 length 成员.

如果仅让上例通过编译, 可以把参数 arg 变成数组. 对于数组来说, 不管它存储的元素是什么类型(T), 数组本身总有 length 成员.
把泛型参数看成普通类型, 声明一个泛型数组类型:

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

函数 loggingIdentity 读作: 有类型参数 T, 接受 T 数组类型参数 arg, 返回一个 T 数组的泛型函数.
如果我们传入 number 数组, Tnumber 绑定, 由此, 返回值也是 number 数组.
类型参数 T 可以传递给其他泛型类型, 带给我们更大灵活性.

我们可以如下重写上个例子:

1
2
3
4
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

这种类型风格常见于支持泛型的语言.
下节, 我们讨论创建你自己的泛型类 Array<T>.

泛型接口

上一节, 我们创建了适应多种类型的 identity 函数.
本节, 我们探讨函数本身的类型, 以及如何创建泛型接口.

泛型函数的类型与普通函数差别不大, 仿照函数定义, 类型参数列在最前:

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

只要类型参数数量相同, 位置一一对应, 函数类型中类型参数的名称可以是任意的.

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

也可以把函数类型写作对象字面量类型的调用签名:

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

这引领我们写下第一个泛型接口.
让我们把以上对象字面量转变成接口:

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn {
<T>(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn = identity;

下个例子的出发点是, 我们可能希望类型参数不仅对其中一个方法, 而对整个接口起作用.
由此我们能够看到整个接口统一于什么类型 (例如: Dictionary<string>Dictionary 直观得多).
接口中所有成员都能引用这个类型参数.

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn<T> {
(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

此例发生了一点小小的变化.
对比修改前, 我们的函数签名是非泛型的, 它现在是一个泛型类型的一部分.
指定类型参数 T (这里: number)来使用 GenericIdentityFn, 实际上同时把调用签名的参数确定了下来.
希望描述类型某部分是泛型的, 就要理解何时把类型参数放在调用签名上, 何时把类型参数放在接口上.

除泛型接口外, 我们也能够创建泛型类.
但是不能创建泛型枚举和名字空间.

泛型类

泛型类有与泛型接口相似的外观.
尖括号包裹的类型参数列表紧跟在泛型类类名之后.

1
2
3
4
5
6
7
8
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

这是 GenericNumber 类一个粗浅用例, 但我们反复强调, T 能代表的类型不受限制.
你可以将它初始化为 number, string, 甚至更复杂的类型.

1
2
3
4
5
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

参考接口, 为类本身添加类型参数可保证它所有引用 T 的成员类型统一.

正如我们在一章阐述的, 一个类有静态面和实例面.
泛型谈的只是一个类的实例面, 静态成员不能引用类的类型参数.

约束类型参数

至此, 你可能希望编写一个泛型函数, 它只允许已知具有特定本领的类型集合初始化其类型参数.
回到 loggingIdentity 函数, 在那里, 我们试图访问 arglength 属性, 而编译器没有同意, 因为它不能证明所有类型都有 length 属性.

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

与其让类型参数代表任何 / 所有类型, 同时失去对实际类型做出任何期望 / 假设的能力, 我们不如约束该函数只为有 length 属性的类型工作.
只要一个类型具有 length 属性, 我们就接受它, 而且, 该属性是必要的.
显然, 我们需要列出一些要求, 这些要求确定 T 应该满足的条件.

在 TypeScript 中, 我们定义接口来描述对类型参数的要求.
这里, 我们创建了一个包含 length 属性的接口, 然后, 用 extends 关键字表达对类型参数 T 的相关约束 Lengthwise:

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}

泛型函数约束了它的类型参数 T, 所以, 不再对任何 / 所有类型通用:

1
loggingIdentity(3);  // Error, number doesn't have a .length property

我们只能向它传递包含所有必要属性的值:

1
loggingIdentity({length: 10, value: 3});

在约束子句中使用类型参数

你可以声明一个类型参数受限于另一个类型参数.
作为例子, 我们希望给定属性名获取对象属性.
我们应当确保不会意外地试图取得一个不存在于对象 obj 中的属性, 不妨如实陈述两类型参数间关系:

译注: keyof 关键字首次出现在高级数据类型一章.

1
2
3
4
5
6
7
8
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

使用类类型

在 TypeScript 中创建泛型工厂函数时, 有必要通过构造器函数引用类类型, 例如:

1
2
3
function create<T>(c: {new(): T; }): T {
return new c();
}

一个更高级的用例是利用 prototype 属性推断和限制类类型构造器函数和实例面的关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BeeKeeper {
hasMask: boolean;
}

class ZooKeeper {
nametag: string;
}

class Animal {
numLegs: number;
}

class Bee extends Animal {
keeper: BeeKeeper;
}

class Lion extends Animal {
keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}

createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
如果这篇文章对您有用,可以考虑打赏:)