泛型(Generics)
软件工程的一个主要部分是建立不仅有良好定义和一致性APIs,而且是可重用的组件(components)。使用今天数据以及明日数据的组件才能够最为灵活地构建大规模软件系统。
在类似C#和Java等语言中,工具箱中创建可重用组件的一个主要工具就是泛型(generics),即能够创建可以使用各种类型而不是单一类型的组件。这使得用户可以用自己的类型来调用这些组件。
Hello World of Generics
我们先来做一个泛型的"hello world":identity函数。identity函数返回传入的数据,可以把这看做类似于echo命令所做的那样。
如果不使用泛型,就必须给identity函数或者传入一个特定的类型例如number:
function identity(arg: number): number { return arg; }
或者传入any类型:
function identity(arg: any): any { return arg; }
虽然使用‘any‘类型当然是泛型,参数‘arg‘可以接受任意类型,但实际上损失了函数返回什么类型的信息。如果传入的是number类型,我们只知道可以返回任意类型。
反之,我们需要能够捕获参数类型,并使用这个类型来表示返回类型。这里,我们将使用一种类型变量(type variable), 描述类型而不是描述值的一种特殊变量。
function identity<T>(arg: T): T { return arg; }
现在我们已经向identity函数添加了一个类型变量‘T‘。这个‘T‘就可以捕获用户输入的类型(例如number), 后续就可以使用这个信息。这里,我们再次用‘T‘作为返回类型。经过检查,我们现在看到参数和返回的都是同一个类型。这样我们就可以在函数内部和外部都可以看到该类型信息。
我们把这个版本的‘identity‘函数称为泛型,是因为它可以使用各种类型。与使用‘any‘不同的是,它也更为精确(即没有丢失任何信息),这是第一个参数与返回类型都可以是 number的‘identity‘函数。
一旦我们编写了泛型identity函数,就可以通过两种方式来调用。第一种方式是传递参数的所有信息,包括参数类型给函数:
var output = identity<string>("myString"); // type of output will be ‘string‘
这里明确设置‘T‘为string,作为函数调用的一个参数,在参数周围使用<>而不是用()来标记。
第二种方式可能最为常见,这里使用/类型参数推断/, 即由编译器根据传入参数的类型来设置T的值:
var output = identity("myString"); // type of output will be ‘string‘
注意这里没有明确用尖括号<>来传递类型,编译器查看"myString"值并设置T为其类型。尽管类型参数推断使得代码更短,可读性更好,但在更复杂的例子中当编译器无法推断类型时,你可能需要像前一个例子那样明确传入类型参数。
使用泛型类型变量(Generic Type Variables)
当开始使用泛型时,你会注意到当创建类似于‘identity‘的泛型函数时,编译器会强加你在函数体中正确地使用任意泛型参数。即你实际处理这些参数时就像它们可以是任意类型一样。
看看前面的‘identity‘函数:
function identity<T>(arg: T): T { return arg; }
如果想要每次调用还能够记录下参数‘arg‘的长度到console,可能会写成这样:
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn‘t have .length return arg; }
如果这样做,编译器会报错‘arg‘没有".length"成员,但现在我们假定‘arg‘有".length"成员。记住,前面我们提到这些类型变量可以表示any以及所有类型,所以使用这个函数的人可能会传入一个 ‘number‘,而这个类型没有".length"成员。
我们实际上希望这个函数用于T的数组而不是T,因为在使用数组,而array类型有.length成员。可以将这描述为我们需要创建其他类型的数组:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg; }
loggingIdentity可以被视为:"泛型函数loggingIdentity,接受一个类型参数T和一个参数‘arg‘,arg是T的一个数组,并返回T的一个数组“。如果传入的是number的一个数组,就会返回一个number数组,这是因为T绑定为number类型。这样我们就可以用泛型类型变量‘T‘作为类型的一部分,而不是全部类型, 这样可以得到更大 的灵活性。
上面的例子还可以写成这样:
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error return arg; }
你可能已经从其他语言熟悉了这种泛型类型。在下一章,讨论如何来创建类似Array<T>的泛型类型。
泛型类型(Generic Types)
在前面章节中,创建了可使用各种类型的identity泛型函数。在这一章中,来深入分析函数类型以及如何创建泛型接口。
泛型函数类型与非泛型函数类型类似,首先是参数类型,类似于函数声明:
function identity<T>(arg: T): T { return arg; } var myIdentity: <T>(arg: T)=>T = identity;
也可以对类型中的泛型类型参数使用不同的名字,只要类型变量的数量与所用的类型变量一一对齐:
function identity<T>(arg: T): T { return arg; } var myIdentity: <U>(arg: U)=>U = identity;
我们也可以将泛型类型写成一个对象字面量(object literal)类型的一个调用签名(call signature):
function identity<T>(arg: T): T { return arg; } var myIdentity: {<T>(arg: T): T} = identity;
这样我们可以写下第一个泛型接口。从前面例子中取出对象字面量(object literal),将它作为一个接口:
interface GenericIdentityFn { <T>(arg: T): T; } function identity<T>(arg: T): T { return arg; } var myIdentity: GenericIdentityFn = identity;
在类似的例子中,我们可能想要将泛型参数移为整个接口的一个参数。这样就可以看清楚是对什么类型来做泛型(例如Dictionary<string>而不只是Dictionary)。这使得类型参数对接口的所有其他成员可见。
interface GenericIdentityFn<T> { (arg: T): T; } function identity<T>(arg: T): T { return arg; } var myIdentity: GenericIdentityFn<number> = identity;
注意我们的例子已经变的有些不同。不是描述一个泛型函数,现在我们有一个非泛型函数签名成为泛型类型的一个部分。当我们使用GenericIdentityFn,现在需要指定对应的类型参数(上面例子中就是number),事实上就锁定了下面的调用签名将使用什么类型。理解什么时候将类型参数直接放到调用签名上,以及什么时候将类型参数放到接口上,有助于描述类型的哪些方面是泛型。
除了泛型接口以外,也可以创建泛型类。注意不可以创建泛型枚举与泛型模块。
泛型类(Generic Classes)
泛型类与泛型接口有类似的形式(shape)。泛型类是在类名后面尖括号(<>)中有一个泛型类型参数。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } var myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; };
这是字面上使用 ‘GenericNumber‘类,但你可能已经注意到并非只能使用‘number‘类型而是可以不受限制,也可以使用‘string‘或更复杂的对象。
var stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; }; alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
就像接口一样,将类型参数放到类上可以确保类的所有属性(properties)都使用相同的类型。
我们在类中已经描述到, 一个类的类型有两方面:静态与实例化。泛型类只能是对实例的泛型,而不是对静态的泛型,所以类的静态成员不能用作类的类型参数。
泛型限制(Generic Constraints)
在前面例子中,有时候想要写一个可适用于多种类型的泛型函数,对这些类型有哪些能力也是知道的。在‘loggingIdentity‘例子中,想要能够访问‘arg‘的".length" property,但编译器不能确保每个类型都有".length" property,所以它会发出告警不能这样假定。
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn‘t have .length return arg; }
并非想要工作于any以及所有的类型,我们可以限制这个函数工作于any以及那些有".length" property的类型。只要类型有这个成员就允许,但需要至少有这个成员。为此,就需要将这个需求作为T的一个限制。
我们可以创建一个接口来描述对类型的限制。这里我们创建一个接口,该接口只有单个".length" property,这样就可以使用该接口和extends关键字来表示限制:
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; }
因为泛型函数现在受到限制,因此不再工作于any与所有类型:
loggingIdentity(3); // Error, number doesn‘t have a .length property
相反,我们需要在传入的值中,其类型有全部所需的properties:
loggingIdentity({length: 10, value: 3});
在泛型限制中使用类型参数(Using Type Parameters in Generic Constraints)
在一些情况下,声明一个类型参数被另一个类型参数限制可能会有用。例如:
function find<T, U extends Findable<T>>(n: T, s: U) { // errors because type parameter used in constraint // ... } find (giraffe, myAnimals);
上面的例子可以用限制来替换类型参数。将上面的例子重写为:
function find<T>(n: T, s: Findable<T>) { // ... } find(giraffe, myAnimals);
注意: 上面两个并非严格一致,因为第一个函数返回类型会返回‘U‘, 而第二个函数没有提供方式返回‘U‘。
在泛型中使用Class类型(Using Class Types in Generics)
在TypeScript中使用泛型来创建工厂时, 构造函数需要引用class类型。例如:
function create<T>(c: {new(): T; }): T { return new c(); }
更为高级的例子使用原型属性(prototype property)来引用并限制构造函数与class类型实例之间的关系:
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 findKeeper<A extends Animal, K> (a: {new(): A; prototype: {keeper: K}}): K { return a.prototype.keeper; } findKeeper(Lion).nametag; // typechecks!
参考资料
[1] http://www.typescriptlang.org/Handbook#generics
[2] TypeScript系列1-简介及版本新特性, http://my.oschina.net/1pei/blog/493012
[3] TypeScript系列2-手册-基础类型, http://my.oschina.net/1pei/blog/493181
[4] TypeScript系列3-手册-接口, http://my.oschina.net/1pei/blog/493388
[5] TypeScript系列4-手册-类, http://my.oschina.net/1pei/blog/493539
[6] TypeScript系列5-手册-模块, http://my.oschina.net/1pei/blog/495948
[7] TypeScript系列6-手册-函数, http://my.oschina.net/1pei/blog/501273