TypeScript手册翻译系列7-泛型

泛型(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

时间: 2024-10-21 11:25:04

TypeScript手册翻译系列7-泛型的相关文章

TypeScript手册翻译系列8-常见错误与Mixins

常见错误 下面列出了在使用TypeScript语言和编译器期间,经常会遇到的一些常见错误. "tsc.exe" exited with error code 1. 解决方案: 检查文件编码为UTF-8 - https://typescript.codeplex.com/workitem/1587 external module XYZ cannot be resolved 解决方案:检查模块路径是大小写敏感- https://typescript.codeplex.com/workit

TypeScript手册翻译系列12-编写.d.ts文件

Writing .d.ts files When using an external JavaScript library, or new host API, you'll need to use a declaration file (.d.ts) to describe the shape of that library. This guide covers a few high-level concepts specific to writing definition files, the

6.Swift教程翻译系列——Swift集合类型

英文版PDF下载地址http://download.csdn.net/detail/tsingheng/7480427 Swift提供数组和字典两种集合类型,用来存储许多值的情况.数组有序的存储一组相同类型的值.字典也存储一组相同类型的值但是是无序的,字典中存储的值可以通过一个唯一的标识(也就是Key)来查找. 在Swift中,数组和字典总是清楚自己能存储的值的类型和key的类型.也就是说你不会错误的把其他不对应的类型存进数组或者字典.所以你也能确定从数组或者字典中取出来的值的类型肯定也不会错了

使用Material Design 创建App翻译系列----材料主题的使用(Using Material Theme)

上一篇是使用Material Design 创建App翻译系列--開始学习篇,进入正题: 新的材料主题提供了下面内容: 1. 提供了同意设置颜色板的系统部件组件. 2. 为这些系统组件提供了触摸反馈动画. 3. Activity的过渡动画. 依据你的品牌标识,使用你所控制的颜色板能够自己定义材料主题的外观. 使用主题的属性能够给ActionBar 和 status bar进行着色. 系统部件拥有新的设计和触摸反馈动画.你能够为你的应用自己定义颜色板.触摸反馈动画以及Activity之间跳转的过渡

《Entity Framework 6 Recipes》中文翻译系列 目录篇 -持续更新

为了方便大家的阅读和学习,也是响应网友的建议,在这里为这个系列做一个目录.在目录开始这前,我先来回答之前遇到的几个问题. 1.为什么要学习EF? 这个问题很简单,项目需要.这不像学校,没人强迫你学习! 我学习EF的原因主要是: a.EF是微软推荐的数据库访问技术: b.能提高我的开发效率,我不喜欢写那密密麻麻的SQL: c.比我写的SQL更合理,更快.目前EF生成的SQL的质量已经很高了.你比较熟悉SQL的话,那它在速度上肯定比不上你,新手的话就别跟我争快慢了,能写一像样的SQL就不错了.至少我

Swift编程语言(中文版)官方手册翻译(第一部分)

独立翻译,进度会比较慢.等不及的可以看CocoaChina翻译小组,他们也正在组织翻译,而且人手众多,相信会提前很多完成翻译.当然质量就见仁见智了.原文可以在iTunes免费下载 目前进度 4 JUN 2014:6.5 % 前言 今天Apple发布了新的编程语言Swift,也提供了一本将近400页的 The Swift Programming Language(Swift编程语言).虽然我没有开发者账号,没法实际上机练习Swift代码,但这不影响我阅读这本书,以及用铅笔在纸上运行这些小程序.Ap

7.Swift教程翻译系列——控制流之循环

英文版PDF下载地址http://download.csdn.net/detail/tsingheng/7480427 Swift提供了类C语言类似的控制流结构.包括for循环和while循环来多次执行任务,if和switch语句根据不同的条件执行不同的分支代码,break和continue语句将执行流程跳转到其他语句. 除了C里面传统的for-条件-递增循环,Swift还增加了for-in循环使得遍历数组,字典,范围,字符串或者其他序列都很简单. Swift的switch语句也要比C语言的sw

RHadoop教程翻译系列 _Mapreduce(1)_第一个Mapreduce任务

如果单从概念上来说,Mapreduce和R中的函数lapply, tapply并无差别,它们都是把元素转化成列,然后计算索引(Mapreduce中的键),最后合并成一个定义好的组合.首先,让我们看一个简单的lappy的例子. small.ints = 1:1000 sapply(small.ints, function(x) x^2) 这个例子比较简单,只是计算了前1000个整数的平方,不过我们可以从这个例子中对lappy这个函数有个基本的认知,接下来关于这个函数还有更多有意思的例子.现在让我们

关于印发《新疆维吾尔自治区外语翻译系列专业技术职务任职资格条件(试行)》的通知

关于印发<新疆维吾尔自治区外语翻译系列专业技术职务任职资格条件(试行)>的通知 新人发[2003]74号 颁布时间:2003-10-21 00:00发文单位:新疆维吾尔自治区人事厅. 新疆维吾尔自治区外事办公室 伊犁哈萨克自治州人事局.外办,各地.州.市人事局.外办,自治区各委.办.厅.局.人民团体.大专院校.科研院所.大中型企业人事(职称)部门,中央驻疆单位人事(职称)部门: 现将<新疆维吾尔自治区外语翻译系列专业技术职务任职资格条件(试行)>印发给你们,请遵照执行.执行中有何意