泛型
软件工程中,我们不仅要创建定义良好且一致的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验)
泛型
:不预先确定的数据类型,具体的类型在使用的时候才能确定。
泛型变量(类型参数)
是代表类型的参数。
泛型的好处
- 函数和类可以支持多种类型,增加的程序的可扩展性
- 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性
- 灵活控制类型之间的约束
基础示例
1 | // 定义一个打印函数 只支持字符串参数 |
使用 any
类型会导致这个函数可以接收任何类型的 arg 参数,但是这样就丢失了一些信息:忽略了输入参数类型和返回值类型必须一致,当调用者看到 log
函数时完全无法获知这种 约束关系
。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。这里,我们使用了 类型参数
,它是一种特殊的变量,只用于表示 类型
而不是 值
。
泛型函数
1 | function log<T>(value: T): T { |
这个版本的 log
函数叫做 泛型
,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
1 | log<string[]>(['a', ',b', 'c']) |
第二种方法更普遍。利用了类型推断
– 即编译器会根据传入的参数自动地帮助我们确定 T 的类型:
1 | log(['a', ',b', 'c']) |
泛型类型
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
1 | // 泛型函数 |
我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
1 | // 泛型函数 |
我们还可以使用带有调用签名的对象字面量来定义泛型函数:
1 | // 泛型函数 |
泛型接口
泛型约束了函数接口
1 | interface Log{ |
上面的例子泛型仅仅约束了一个函数,我们甚至可以把泛型参数当作整个接口的一个参数,来约束接口的其他成员。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>
而不只是 Dictionary
)。这样接口里的其它成员也能知道这个参数的类型了。
当泛型变量约束了整个接口时,在实现时必须指定类型。或在接口定义时指定一个默认类型。
1 | interface Log<T> { |
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用( <>
)括起泛型类型,跟在类名后面。
泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
实例化时可以指定类型,相应的方法,属性会受到泛型约束。
1 | class Log<T> { |
泛型约束
在泛型函数内部使用类型变量时, 由于事先并不知道它是那种类型, 所以不能随意操作它的属性和方法:
1 | function log<T>(value: T): T { |
上述函数中 类型 T 上不一定存在 length 属性, 所以编译的时候就报错了。
这时,我们可以的对泛型进行约束,对这个函数传入的值约束必须包含 length 的属性, 这就是泛型约束:
1 | interface Length{ |
我们定义一个接口来描述约束条件,创建一个包含 .length
属性的接口,使用这个接口和 extends
关键字来实现约束。
在泛型约束中使用类型参数
你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。
1 | function getProperty<T, K extends keyof T> (obj: T, key: K ) { |
总结
泛型不仅可以保持类型的一致性,又不失程序的灵活性,同时也可以通过泛型约束,控制类型之间的约束。从代码的上来看,可读性,简洁性,远优于函数重载,联合类型声明以及 any 类型的声明。