Dart语言程序设计

978-7-115-29694-8
作者: 【美】Chris Strom
译者: 韩国恺
编辑: 杨海玲

图书目录:

详情

本书是第一本关于Dart语言的中文书籍,介绍了当前Dart语言的最新内容。书中涵盖了Dart语言基础、并发编程、Web编程和HTML5应用等内容。本书从第1页开始就给出真正的代码和实际项目,通过指导读者编写一个简单的应用来探究Dart语言的复杂细节。

图书摘要

·新·锐·编·程·语·言·集·萃·

Dart语言程序设计

【美】Chris Strom 著

韩国恺 译

人民邮电出版社

北京

本书是第一本关于Dart语言的中文书籍,介绍了当前Dart语言的最新内容。书中涵盖了Dart语言基础、并发编程、Web编程和HTML5应用等方方面面的内容。

因为Dart语言让人感觉非常熟悉,与一般编程语言的书通常以“Hello World”开篇不同,本书一开始就带领读者编写一个 Ajax 功能的应用程序,然后详细讨论 Dart的基本类型,把 Dart 编译为 JavaScript,面向对象的编程方法,并构建一个易于使用和维护的库,最后介绍在Dart中如何使用HTML5编程。

作者通过真实的项目,引领读者用Dart解决实际问题。每一个项目作为进一步深入讨论Dart语言特性的基础。为了增强对Dart语言的理解,项目会逐渐深入,并且越来越复杂。读完整本书后,读者不仅获得全面的Dart语言的知识,而且还从头构建了一个完整的MVC库。

本书适合编程语言爱好者和Web开发者阅读。

对于任何想要了解什么是Dart语言,以及如何将它与当前浏览器结合起来使用的人,这都是一本有趣且容易阅读的书。对Dart语言计划发布特性的评论,给了你买这本书足够的理由。

——Matt Margolis

起初我有点儿怀疑Dart语言。这本书让我了解到Dart语言的前景和当前的状态,它将作为我可以依赖的一本可靠的参考书目。

——Juho Vepsäläinen

这是第一本介绍Dart这种令人兴奋且十分有前途的编程语言的书。清晰和平易近人的文字很吸引读者,它肯定会对Dart语言的成功有所贡献。我特别喜欢作者对这种语言函数式方面论述以及对Isolate的讨论。

——Dr. Ivo Balbaert

2011 年10月,Google发布了一种新的编程语言—Dart,主要目标是将其作为一种结构化的Web开发语言。Dart语言既让人感觉熟悉,又足够灵活;既适用于快速原型开发,又适用于组织大型的代码库;既可以用在桌面版和移动版的浏览器中,也可以在服务器端使用。

Dart是一门很新的语言,自发布以来发展速度很快。这要感谢Google的大力支持和社区的积极反馈。说它很新,是因为在翻译此书时,Dart发布还不满一年,甚至还没有发布第一个正式版(但是很快会发布第一个里程碑版M1)。说它发展很快,是因为这一年来频繁地发布更新,很多方面都在不断地完善和改进,甚至我在翻译此书时都要不断跟进Dart语言的最新版本,并对原书的内容做更新和修订。

正是由于这两点,本书与其他编程语言书籍明显不同。我深知技术爱好者对最新内容的渴望,所以翻译此书时我给自己定的一个目标就是要保持书中的内容最新。通过对原书内容的大量修订和补充,以及对示例代码的实际验证和修改,尽可能保证本书在出版时能够与Dart语言的最新版本一致。

Dart是一门很特别的语言。我觉得其最有特色的特性是可选类型,在动态语言的基础上,结合了静态语言的优点。另一个特色是,Dart中的类和接口是统一的,每个类都有一个隐式接口。除此之外,还有很多非常不错的特性,比如工厂构造函数和命名构造函数、getter/setter 方法、语言级别的级联调用等。作为现代语言的基本功能, Dart自然也包含了良好的面向对象和并发编程的支持,未来可能还支持Mixin和基于镜像的反射。

编程语言并不是孤立存在的,Dart也是这样,它由语言规范、虚拟机、类库和工具组成。因为Dart要运行于其他浏览器中,所以dart2js编译工具也是语言核心的一部分。Dart Editor是Dart的集成开发环境,包含了语言分析工具。Dart的可选类型带来的一个好处就是,它可以像静态语言一样得到IDE的良好支持,而这就是由语言之外的独立分析工具提供的。

尽管本书篇幅不大,但内容比较全面。同时以实践性的项目章节贯穿全书。

这是我翻译的第一本书,翻译中出现错误在所难免,请读者见谅并反馈给我。另外,还要感谢出版社和编辑给予的大力支持。

韩国恺

2012年10月17日

为什么使用Dart

当我问这个问题时,我并不想知道Google为什么正在致力于Dart,我也不是想问这门语言的设计者希望实现什么。当然,在这本书中我们将谈及这些问题和更多的内容。

我也常常问自己:“为什么使用Dart?”到底是什么让我认为这是一门值得学习的好语言,甚至愿意单独为这门语言写一整本书?特别是当前才发布了0.08版

【注】① 翻译此书时为0.10版。—译者注

这个问题的答案与我的个人及职业发展轨迹有关——理解如何使互联网更快。回到那段日子,我还是一名简简单单的Perl程序员。我很喜欢Perl这门语言,并用它做些我想做的东西。但是,当接触了Ruby和Ruby on Rails之后,我震惊了。简单、清晰的代码和强约定的组合赢得了我的青睐,而且持续了相当长的一段时间。

之后,我尝试了小型框架,如Sinatra ,它保留了Ruby语言的美,但是却带来更小、更快的代码。这两种框架都满足了Web开发的连续性,我当时也很满意。

【注】② http://sinatrarb.com,一种Ruby语言实现的用于快速创建Web应用的DSL。—译者注

但是还能有更多的选择吗?这最终把我带到了 Node.js 以及在其上构建的各种JavaScript框架,而且看起来很难再对服务器端进行改进。

然后,我发现了SPDY 协议,它令我着迷,以至于写了The SPDY Book 这本书。这不仅是改进现有的东西,而且是尝试重新定义游戏规则。

【注】③ http://www.chromium.org/spdy。

【注】④ http://spdybook.com/。

在我接触SPDY的时候,我注意到了一件事,无论我利用多少这种协议所提供的优势,Web应用的最终速度还是受限于处理网页和客户端脚本、CSS等的速度。

JavaScript已经有 17年的历史了。在它首次被引入时,还没有Web 2.0、Ajax、CSS,而且根本没有多少客户端交互。当JavaScript首次出现时,主要的使用场景就是验证表单并用警告框提示!

在接下来的17年里,JavaScript语言已经从网景公司拥有并缓慢开发的一种专有语言,演化为一个定期添加新特性的Web标准。但是与委员会添加新特性到标准中相比,Web演化的速度明显更快。

然后Dart语言到来了。Dart问:考虑到我们现今所知的Web,我们如何从头开始构建 JavaScript?怎样才能尽可能快地加载和运行?如何编写才能使我们很容易地定义和加载外部库?

我们怎样才能使开发者轻松地写出漂亮的代码?

如果Dart语言是这许多问题的答案(并且我将实际尝试它们),那么Dart很可能是很长一段时间内最令人兴奋的技术。

这就是对“为什么使用Dart?”的回答。

谁应该阅读本书(除了技术达人)

这本书主要是为那些想让自己的 JavaScript 技能保持最新的开发者们写的。提高JavaScript技能的最好途径就是亲身实践和阅读别人的代码。但是,有时看看竞争者正在忙什么也会有根本上的帮助。既然这样,随着我们探索Dart语言带来了什么,我们能够更好地理解这种优秀语言的与众不同之处。

我也希望这本书会证明这种新的转变是有用的。对于当今的浏览器,Dart语言已经是一个可用于构建快速Web应用的有价值的平台。我希望你在读完这本书后,能够很好地武装起来去开发下一代Web应用。

这本书的目标读者应该是喜欢学习各种编程语言的开发者。我关注了Dart语言的很多方面,特别是那些令我兴奋和高兴的地方。

当然,技术达人也应该读这本书。Dart语言相当与众不同,并且足够强大,这使它正吸引着经典的语言技术达人,值得那些想改变世界的人们花时间阅读。

本书的组织

我想做出一些不同的尝试。我没有每章介绍语言的一部分。每个部分都是从一个实际的Dart项目开始的,包括一些特意选择的说明。在这些部分中,我的目标是通过给出这种语言的真实感觉,展现出Dart语言所声称的重要改进。因为这些都是真实的项目,所以很适合指出Dart语言的强项,当然也包括一些弱项。

每一个这样的项目章的后面都会跟着几个更深入语言某个方面的特定主题的章。我使用这些章来涵盖项目章需要更详细解释的地方,以及不能在当前Dart语言参考资料中找到的东西。

所以,如果你需要快速的语言介绍,你完全可以单独地从项目章节开始。如果你想要更传统方式的书,那么跳过项目章而只阅读主题章,或者全部阅读—我保证它值得你花时间!

第一个项目在第1章。对于这个项目的补充在第2章、第3章、第4章和第5章。

下一个项目在第9章,这个项目源于第1章的这个简单Ajax应用程序,并把它提炼为一个羽翼丰满的 MVC 库。如果你想要领略一门语言,编写一个程序库,特别是MVC库,是最好的方式。与MVC库相关的还有第7章、第10章和第8章。

紧接着,我们将在第 11 章看一下 Dart 语言中的依赖注入。与 JavaScript 不同, Dart语言并不是主要作为一种动态语言,尽管如在第11章所看到的那样,它仍然可能实现一些传统动态语言的技巧。在这个项目之后是关于Dart语言测试的介绍,这是一个重要的主题,尽管不是完全源自Dart。

最后一个项目在第13章,在这里我们探索作为传统传递回调函数的高层次的替代—Dart“Future”。这引起了第14章对代码隔离和消息传递的讨论。

最后,我们以各种HTML5技术的简短探索来结束本书。

本书不包含的内容

本书不涵盖Dart Editor的内容。在某种程度上看,算是一点儿缺失。Dart这样的强类型语言具有代码自动完成的能力,Dart Editor便可提供这种功能。不过,本书的重点是语言本身,而不是其相关的工具。此外,一些人(包括我自己)会坚持用他们自己选择的代码编辑器。

这本书的目的不是作为语言参考手册。为这种发展中的语言做参考手册还太早了。不过,希望本书能成为(对开发者不够友好的)语言规格说明书 和(有些地方尚不完整的)API文档 的一个有力补充。

【注】① Dart语言规范在http://www.dartlang.org/docs/spec/。它主要是给语言实现者看的,但必要时也对应用开发者有用。

【注】② http://api.dartlang.org。

关于未来

因为Dart语言还会继续发展,根据Dart语言的变化速度,本书的内容可能一年有一次或两次的评审,然后做相应的修订、删除或补充。如果您发现任何错误或需要改进的地方,请提交到公共问题跟踪上:https://github.com/dart4hipsters/dart4hipsters.github. com/issues。关于新主题方面的建议也非常欢迎。

本书的约定

类名按骆驼式命名(camel-cased),例如HipsterModel。类对应的文件名和类名一样,例如HipsterModel.dart。变量名按蛇式命名(snake-cased ),例如background_color,而函数和方法名都是小写字母开头的骆驼式命名,例如routeToRegExp()。

【注】③ 一种以下划线连结单词的命名方式。—译者注

让我们开始吧

正如前文所说的,让我们开始编写没有历史包袱的Web代码吧。让我们编写Dart代码!

Dart 有很多特点,但也许最令人印象深刻的是,它让熟悉 JavaScript 的程序员感到非常熟悉。在前面的几章中,我们从头开始,编写一个Dart应用程序。

大部分编程书籍都以一个“Hello World!”示例开始。我要说,换种方式——我们这里都是编程达人。让我们开始写代码吧!

首先因为Dart语言写起来感觉比较熟悉,所以我们在深入之前不应该走得太远。让我们直接跳到更有趣的事情上:一个带Ajax的网站。任何一个真正的达人都会收集大量的漫画书(我说的对吗?不是就我一个人这样吧,哈),那么让我们来考虑一个简单的Dart应用程序,通过REST风格的接口来操作一组漫画书。

在某种程度上,这可能有点儿太激进了。别担心,我们会在后续章节中深入细节。

本章的示例代码可以在https://github.com/eee-c/dart-comics的“your_first_dart_app”分支上找到。作为技术爱好者,我们已经在用Node.js了,所以后端需要Node.js和npm。说明包含在项目的README中。

作为REST风格,这个应用程序应该支持下面这些操作:

• GET /comics(返回漫画书的列表);

• GET /comics/42(返回一本漫画书);

• PUT /comics/42(更新一本漫画书);

• POST /comics(在集合中新建一本漫画书);

• DELETE /comics/42(删除一本漫画书)。

我们不用太关心除此之外的后端部分的细节。

我们的整个应用程序将遵循最近的客户端 MVC 框架的惯例。因此,我们只需一个Web页面。

your_first_dart_app/index.html

<!DOCTYPE html>

<html>

<head>

  <title>Dart Comics</title>

  <link rel="stylesheet" href="/stylesheets/style.css">

  <!-- 强制让Dartium启动脚本引擎 -->

  <script language="text/javascript">

  navigator.webkitStartDart();

 </script>

  <!-- 主程序脚本 -->

  <script src="/scripts/comics.dart"

  type="application/dart"></script>

</head>

<body>

  <h1>Dart Comics</h1>

  <p>Welcome to Dart Comics</p>

  <ul id="comics-list"></ul>

  <p id="add-comic">

   Add a sweet comic to the collection.

 </p>

</body>

</html>

对于大家而言,这个Web页面的大部分应该很熟悉。它包含了简单的HTML、CSS的链接和脚本。

1.HTML头部

唯一要注意的有点儿奇怪的地方是第一个<script>标签。在这个标签中,用JavaScript来启动Dart脚本引擎。

your_first_dart_app/_index_force_dartium_script_engine.html

<!-- 强制让Dartium启动脚本引擎 -->

<script language="text/javascript">

 navigator.webkitStartDart();

</script>

要点:在写这本书时,在Dartium上必须用navigator.webkitStartDart()来启用Dart VM,Dartium是包含Dart语言支持的Chrome版本。这个要求可能会在不久的将来去除。

【注】① http://www.dartlang.org/dartium/。

接下来,我们加载了实际代码的内容。这里唯一的变化是<script>标签的type属性,用来表示这是Dart代码。

your_first_dart_app/_index_src_dart.html

<!-- 主程序脚本 -->

<script src="/scripts/comics.dart"

type="application/dart"></script>

第10章还有更多关于Dart加载库和包含代码的内容。现在只要知道它会按照我们期望的方式加载Dart即可。

2.HTML主体

作为HTML的主体部分,没什么新东西,不过我们应该注意一下将附加上行为的两个元素的ID。

your_first_dart_app/_index_body.html

<h1>Dart Comics</h1>

<p>Welcome to Dart Comics</p>

<ul id="comics-list"></ul>

<p id="add-comic">

  Add a sweet comic to the collection.

</p>

对#comics-list UL 元素,我们将附加上后端数据存储中的漫画书列表。#add-comic段落标签上将附加上一个表单处理程序。那么,我们就开始吧。

在scripts/comics.dart中,我们的Dart应用程序以两个Dart库的加载和一个main()函数开始。

your_first_dart_app/comics_initial_main.dart

import 'dart:html';

import 'dart:json';

main() {

 load_comics();

}

load_comics() {

 // 在这里具体实现

}

正如我们将在第10章看到的,import语句有很多功能。现在我们只要简单地认为它导入了Dart核心行为之外的其他功能即可。

所有的Dart应用程序都是以main()作为执行的入口点。在JavaScript中,我们只要直接写代码然后就可以运行,但在Dart中这样不行。起初这可能看起来类似C系列的语言,但这确实是有意义的,难道要让遍布于各处的 JavaScript 源文件和 HTML文件中的所有代码都立刻执行?在 Dart 语言中,main()入口点不只是约定,它是由这一语言强制的最佳实践。

【注】① 在一个页面中,出现的所有独立js文件或嵌入在HTML文件中的JavaScript都会立即被解析执行。但我们往往需要一个开始执行的入口点,一般是在文档准备好时执行一个函数,如jQuery的$(document).ready(handler)。—译者注

至于load_comics()函数,我们一步步来。首先我们需要标识出列表需要附加的DOM元素(#comics-list),接着我们需要一个Ajax调用来填充这个DOM元素。为了实现这两件事,我们最初的Dart代码看上去大概像下面这样:

your_first_dart_app/_load_comics.dart

load_comics() {

  var list_el = document.query('#comics-list');

 ajax_populate_list(list_el);

}

除了明显地忽略了function关键字之外,这个例子就像是JavaScript代码!在第3章,我们将介绍更多差异。Dart语言仍然用我们熟悉和喜欢的分号和大括号——语言设计者有意让人对这门语言感到很熟悉,至少在表面上看起来很熟悉。

注意:和JavaScript不同,在Dart中分号不是可选的。

除了熟悉,这段代码一看就很容易阅读和理解。没有怪异的、遗留的DOM方法。我们用document.query()而不是用document.findByElementId()来查找元素,并且用#comics-list这种CSS选择器的方式,正如我们在jQuery中已经习惯的那样。

现在已经找到了需要填充的UL元素,让我们来看看如何产生一个Ajax请求。就像在JavaScript中一样,我们要创建一个新的XHR对象,并添加了一个请求加载完成的处理程序,然后打开并发送这个请求。

【注】① 为了减少名称中的冗余信息,在新版的Dart中,过去的XMLHttpRequest已改为HttpRequest,相关的名称也是如此,如XMLHttpRequestException已改为HttpRequestException。—译者注

your_first_dart_app/_ajax_populate_list.dart

ajax_populate_list(container) {

  var req = new HttpRequest();

  req.on.load.add((event) {

   var list = JSON.parse(req.responseText);

   container.innerHTML = graphicNovelsTemplate(list);

 });

 // 动作, 资源, 异步模式

  req.open('get', '/comics', true);

 req.send();

}

对于过去做过Ajax编程的人,对大部分代码应该立刻就能看懂。我们打开创建的XHR对象,指定要获取的资源,然后实际发送请求。

当添加事件处理程序时,我们看到与JavaScript的使用方式有所不同。XHR对象有一个 on 属性,它列出了所有支持的事件处理程序。我们访问其中的一个 load 处理程序类型,这样我们就能用add()方法添加一个处理程序。在这种情况下,我们解析获得的JSON数据为一个散列值的列表。它大概就像这样:

your_first_dart_app/comics.json

[

 {"title":"Watchmen",

  "author":"Alan Moore",

 "id":1},

  {"title":"V for Vendetta",

  "author":"Alan Moore",

 "id":2},

 {"title":"Sandman",

  "author":"Neil Gaiman",

 "id":3}

]

然后,我们要实现这个简单的Dart应用程序的最后一部分——一个填充漫画书列表的模板。

your_first_dart_app/_graphic_novels_template.dart

graphic_novels_template(list) {

  var html = new StringBuffer();

  list.forEach((graphic_novel) {

  html.add(_graphic_novel_template(graphic_novel));

 });

  return html.toString();

}

_graphic_novel_template(graphic_novel) {

  return """

   <li id="${graphic_novel['id']}">

   ${graphic_novel['title']}

   by

   ${graphic_novel['author']}

  </li>

  """;

}

第一个函数简单迭代这个漫画书列表(在心里,我把它们当做漫画小说),构建为一个HMTL字符串

【注】① Dart语言现在已不支持字符串拼接(+)操作,所以需要改用 StringBuffer。简单(非循环)的字符串拼接,如str1+str2,可以用字符串内的变量插值实现,如“$a $b”。或者使用相邻字符串的方式,在Dart语言中相邻的字符串会自动拼接,包括多行情况。—译者注

第二个函数展示了两个Dart语言特性:多行字符串和字符串变量插值。多行字符串由3个引号表示(单引号或双引号)。在字符串中,我们可以用美元符号插入值(甚至一个简单表达式)。对于简单的变量插入,大括号是可以省略的:$name和${name}是一样的。而对更复杂的插入,如散列查找,大括号就是必需的。

就是这样!我们准备好了一个功能完备的、带Ajax的Web应用程序。汇总的代码如下:

your_first_dart_app/comics.dart

import 'dart:html';

import 'dart:json';

main() {

 load_comics();

}

load_comics() {

  var list_el = document.query('#comics-list');

 ajax_populate_list(list_el);

}

ajax_populate_list(container) {

  var req = new HttpRequest();

  req.on.load.add((event) {

   var list = JSON.parse(req.responseText);

   container.innerHTML = graphicNovelsTemplate(list);

 });

 // 动作, 资源, 异步模式

  req.open('get', '/comics', true);

 req.send();

}

graphic_novels_template(list) {

  var html = new StringBuffer();

  list.forEach((graphic_novel) {

  html.add(_graphic_novel_template(graphic_novel));

 });

  return html.toString();

}

_graphic_novel_template(graphic_novel) {

  return """

   <li id="${graphic_novel['id']}">

   ${graphic_novel['title']}

   by

   ${graphic_novel['author']}

  </li>

  """;

}

页面加载后看上去像这样:

这便是我们探索Dart语言的良好开端。的确,我们这里掩饰了许多成就Dart语言的地方。但是这样做,让我们有了一个使用Ajax的Web应用程序的良好开始。最棒的是,我们写的代码看起来似乎与 JavaScript 没有什么不同。一些语法比我们使用JavaScript 更整洁一点(没人会抱怨整洁的代码),并且这些字符串特性也很不错。但是总的来说,可以肯定的是,使用相对更短的Dart语言是有生产力的。

在写作本书时,这个应用还不能在(几乎)任何地方实际运行。

任何浏览器(甚至Chrome)都还不支持Dart语言。为了原生地运行这个Web应用程序,我们需要安装Dartium——一个嵌入Dart VM的Chrome分支版本。Dartium可以从Dart语言网站获得

【注】① http://www.dartlang.org/dartium/。

即使在Chrome支持Dart语言之后,我们仍然面临着只能被市场上个别浏览器支持的情况。这有点儿糟糕。

好在Dart能够编译为JavaScript,这意味着你可以在利用Dart能力的同时对所有平台可用。为了更容易实现这一点,我们添加一个小的 JavaScript 库,用来探测浏览器是否支持Dart语言,如果不支持就加载编译成JavaScript的等价物。

your_first_dart_app/_index_src_js_fallback.html

<!-- 启用回退到Javascript -->

<script src="/scripts/conditional-dart.js"></script>

第5章中将讨论这个帮助文件的细节。目前,只要知道我们的Dart代码不是锁定在一个浏览器厂商那里就足够了。我们肯定不会回到VBScript那样。

不可否认,这是对Dart语言的快速入门。能够如此快速地搭建和运行真是太棒了。仿佛我们此刻就获得了生产力。

尽管如此,我们才刚刚开始学习Dart语言,别搞错了,我们的Dart代码还可以改进。因此,让我们用下面的几章来熟悉Dart语言中的一些重要概念。之后,在第6章中,我们将准备好把这个Dart应用转变成MVC的方式。

本书中一个反复出现的主题是,Dart语言就是要让人觉得很熟悉。如果做到了这一点,那么对这一语言的基本组成部分的论述应该是相对简短的,并且确实是这样。尽管如此,对一些核心类型的介绍还是有帮助的。自然,其中也会有几处“陷阱”。

整数和小数都是数字类型,这意味着它们支持很多相同的方法和运算符。Dart语言中的数字类型与许多其他语言中的数字类型非常像。

【注】① 在Dart中,int和double都继承自num。—译者注

2 + 2;  // 4

2.2 + 2; // 4.2

2 + 2.2; // 4.2

2.2 + 2.2; // 4.4

可以看出,在二者混合运算时Dart语言的数字类型做了“正确的事”。

字符串是不可变的,换种直观的说法就是,字符串的操作会产生新的字符串而不是修改现有的字符串。字符串(像数字一样)是可散列的,这意味着唯一的对象有唯一的一个散列值来区分彼此。如果我们将一个字符串变量赋值给另一个变量,它们将有一样的散列值,因为它们是同一个对象。

【注】① 严格地说,散列值并不要求一定唯一,但应该很好地分布。如果两个对象相等(equals),那么二者的散列值也应该相等,但反之不一定成立。也就是说,不同内容的字符串的散列值也有可能相同,只是概率很小。—译者注

var str1 = "foo",

str2 = str1;

str1.hashCode; // 425588957

str2.hashCode; // 425588957

但是,如果我们修改了第一个字符串变量,那么它将获得一个全新的对象,而另一个字符串对象还是指向原来的字符串。

str1 = "bar";

str1.hashCode; // 617287945

str2.hashCode; // 425588957

Dart语言使字符串的处理更容易。例如,可以用三重引号括起来的方式新建多行字符串。

"""Line #1

Line #2

Line #3""";

Dart语言也支持相邻字符串拼接。

'foo' ' ' 'bar'; // 'foo bar'

这种相邻字符串的便利用法甚至扩展到多行字符串上。

'foo'

' '

'bar'; // 'foo bar'

最后一个Dart字符串的便利用法是字符串内的变量插值。Dart语言使用$表示要插值的变量。

var name = "Bob";

"Howdy, $name"; // "Howdy, Bob"

如果在变量表达式结束和字符串开始的地方存在混淆,可以将大括号和$一起使用。

var comic_book = new ComicBook("Sandman");

"The very excellent ${comic_book.title}!";

// "The very excellent Sandman"

在Dart语言中,布尔类型(bool)的值只允许是true和false。在Dart语言中“真值”和它自身的概念一样简单:如果不是true,那就是false。考虑下面的代码:

var name, greeting;

greeting = name ? "Howdy $name" : "Howdy";

// "Howdy"

/*** Name仍然不是true ***/

name = "Bob";

greeting = name ? "Howdy $name" : "Howdy";

// "Howdy"

greeting = (name != null) ? "Howdy $name" : "Howdy";

// "Howdy Bob"

如果你用过许多其他语言,那么不会奇怪在布尔上下文中 null、""和 0 的值为false。你可能也习惯于"Bob"和42的值为false。

“真值”的语义运行在“类型检查”模式下会稍有不同(见 2.7 节),但是最好不要依靠这种差异。如果我们总是用最后那种写法,那么我们就肯定不会犯错了。

在第 7 章中,我们将探讨操作符定义,这允许我们自定义特定类的 equals/==方法的含义。这给Dart语言关于布尔类型带来了一定的灵活性。

在Dart语言中,键值对的数据结构是由HashMap对象实现的。下面的代码定义了一个叫options的HashMap变量:

var options = {

  'color': 'red',

  'number': 2

};

如你所料,用方括号从HashMap中获取值:

var options = {

  'color': 'red',

  'number': 2

};

options['number']; // 2

HashMap实现了Map接口,它的大多数API都定义在这里。这包括关于获取键(keys)、获取值(values)以及用forEach()迭代整个对象的信息。

【注】① http://api.dartlang.org/dart_core/Map.html。

var options = {

  'color': 'red',

  'number': 2

};

options.forEach((k, v) {

  print("$k: $v");

});

// number: 2

// color: red

注意:键值对的顺序没有保证。

HashMap的一个非常有用的特性是putIfAbsent()方法。下面两段代码是等价的:

// 普通的实现

if (!options.containsKey('age')) {

  var dob = new Date.fromString('2000-01-01'),

   now = new Date.now();

  options['age'] = now.year - dob.year;

}

// 更好的实现

options.putIfAbsent('age', findAge);

findAge() {

  var dob = new Date.fromString('2000-01-01'),

   now = new Date.now();

  return now.year - dob.year;

}

在第一个例子中,条件语句和代码块都与 options 对象的实现细节相关。但在第二个例子中,findAge()函数只与计算当前年龄相关,而options对象则只关心添加值。

要点:应该总是考虑使用putIfAbsent()方法,这样Dart程序会更清晰。

第一个例子也可以改成下面这样:

if (!options.containsKey('age')) {

  options['age'] = findAge();

}

但是,不用putIfAbsent()方法的话,似乎只能把findAge的实现放到条件语句中。无论怎样,条件语句的格式永远不会像等价的 putIfAbsent()方法这样清晰。

options.putIfAbsent('age', findAge);

putIfAbsent()——学习它,喜欢它。它会节省你的生命(当然,可能不会,但至少会让生活更轻松)。

任何语言都需要表示一个事物的列表。为使开发者容易掌握,Dart语言的列表基本符合你所期望的那样。

var muppets = ['Count', 'Bert', 'Snuffleupagus'];

var primes = [1, 2, 3, 5, 7, 11];

// 索引从0开始

muppets[0]; // 'Count'

primes.length; // 6

Dart语言确实提供了一些不错的并且一致的方法来操作列表。

var muppets = ['Count', 'Bert', 'Ernie', 'Snuffleupagus'];

muppets.setRange(1, 2, ['Kermit', 'Oscar']);

// muppets => ['Count', 'Kermit', 'Oscar', 'Snuffleupagus']

muppets.removeRange(1, 2);

// muppets => ['Count', 'Snuffleupagus'];

muppets.addAll(['Elmo', 'Cookie Monster']);

// muppets => ['Count', 'Snuffleupagus', 'Elmo', 'Cookie Monster']

同时也有一些内建的迭代方法。

var muppets = ['Count', 'Bert', 'Ernie', 'Snuffleupagus'];

muppets.forEach((muppet) {

  print("$muppet is a muppet.");

});

// =>

// Count is a muppet.

// Bert is a muppet.

// Ernie is a muppet.

// Snuffleupagus is a muppet.

muppets.some((muppet) => muppet.startsWith('C'));// true

muppets.every((muppet) => muppet.startsWith('C'));// false

muppets.filter((muppet) => muppet.startsWith('C'));// ['Count']

要点:写作此书时,Dart语言当前缺少reduce()和fold()这样的能够执行高阶操作的列表方法。

【注】① 当前List已支持map和reduce方法。——译者注

太好了,对于Dart语言的列表和数组没有太多需要介绍的了。它是Dart语言中众多“刚好够用”的事物之一。

集合(Collection)

上一节中使用的迭代方法其实不是定义在List类上的。而是来自List的超类:Collection。Collection家族的另两个成员是Set和Queue。

Set是内部元素唯一的集合,并且拥有一些数学集合的操作方法。

var sesame = new Set.from(['Kermit', 'Bert', 'Ernie']);

var muppets = new Set.from(['Piggy', 'Kermit']);

// 因为Ernie已经在集合中了,所以不会插入

sesame.add('Ernie');   // => ['Kermit', 'Bert', 'Ernie']

sesame.intersection(muppets); // => ['Kermit']

sesame.isSubsetOf(muppets); // => false

Queue是一种可以在首尾两端操作的集合。

var muppets = new Queue.from(['Piggy', 'Rolf']);

muppets.addFirst('Kermit');

// muppets => ['Kermit', 'Piggy', 'Rolf']

muppets.removeFirst();

muppets.removeLast();

// muppets => ['Piggy']

根据现在的Queue推论,常规的列表不能操作开头的元素。也就是说,List没有shift或unshift的方法

【注】① 除了支持的方法不同外,当前文档中没有清楚地说明 List 和 Queue 的区别。但是,目前可以认为 Queue默认是由双向链接的列表实现的,所以首尾操作的效率较高;而 List 默认是动态增长的数组,因此二者对于不同类型的操作性能表现是不同的。—译者注

在浏览器上Dart语言对日期和时间带来了一些非常期盼的改进。关于日期方面有一个令许多JavaScript开发者感到痛苦的问题,而在Dart语言中第一个月是从1开始的。让我们开始庆祝吧。

【注】② 在JavaScript和Java等语言中,第一个月是从0开始的,12个月分别是0~11。这种设定不直观,也容易犯错。—译者注

Dart语言中日期的改进还不止这些。例如,有多种创建日期对象的方法。

var mar = new Date.fromString('2012-03-01 14:31:12');

【注】③目前Dart API文档没有准确说明支持哪些字符串格式用于新建Date。—译者注

// 2012-03-01 14:31:12.000

var now = new Date.now();

// 2012-03-10 01:02:24.149

var apr = new Date(2012, 4, 1, 0, 0, 0, 0);

// 2012-04-01 00:00:00.000

更好的是,不仅可以操作日期,而且相当好用。

var mar = new Date(2012, 3, 1, 0, 0, 0, 0);

var apr = new Date(2012, 4, 1, 0, 0, 0, 0);

var diff = apr.difference(mar);

diff.inDays; // => 31

apr.add(new Duration(days: 15)); // => 2012-04-16

【注】① Date的add()方法返回一个新的Date对象,当前对象并不改变。—译者注

Date 中的 difference()方法返回一个封装了一段时间的 Duration 对象。Duration对象支持从“天”到“毫秒”的各种时间单位的查询。正如在add()方法示例中所见,Duration对象也很适合从一个特定的日期开始增加或减少时间。

在Dart语言中使用日期并不是一件可怕的事。正如我们所见,这很轻松。

要点:强烈建议在类型检查模式下完成 Dart 代码的编写。默认情况下,Dart 运行在“生产”模式下,在生产模式下当出现表面上的类型定义冲突时程序不会崩溃。这样就只能依靠开发者自己在开发过程中捕获尽快多的此类问题。要启用类型检查模式,在命令行中用DART_FLAGS='--enable_type_checks --enable_asserts'/path/to/dartium参数启动Dartium。

【注】② Dart语言有两种运行模式,即检查模式(checked mode)和生产模式(production mode),默认运行在生产模式下。本书原文采用的是“type-checked mode”表示检查模式,这是Dart语言早期的说法,现在一般采用“checked mode”的说法,这里还按照原文的“类型检查模式”翻译。—译者注

【注】③ Dart VM和Dartium本身默认都是在生产模式下运行的,而在Dart Editor里,所有(命令行和Web)启动类型默认都是在检查模式下运行的,可以在启动选项中设置。—译者注

【注】④ 官方建议在开发环境中使用检查模式运行,在生产环境下使用生产模式运行。检查模式可以帮你尽早发现问题,但会影响程序性能,所以正式的环境应该用生产模式运行。—译者注

在转移到其他主题之前,让我们快速了解一下Dart语言中的变量声明。到目前为止,我们都是按照JavaScript中的惯例用var关键字来声明变量的。在Dart语言中,var表示可变类型。换句话说,我们不仅没有指定类型,而且还告诉解释器这个类型可能会改变。

var muppet = 'Piggy';

// Dart类似JavaScript,允许这样做,但是更进一步!

muppet = 1;

Dart语言一定程度上是强类型语言,这意味着它更喜欢我们用实际的类型声明而不是var。

【注】① Dart是动态类型语言,也是强类型语言,类似于Ruby。—译者注

String muppet = 'Piggy';

// 在类型检查模式下会失败

// 其他地方会抱怨(在Dart Editor中会给出警告)

muppet = 1;

Dart语言也允许我们做一些傻事,就像前面那段代码中那样,但是它会给出警告。在命令行中可以启用类型检查模式,在类型检查模式下,前面那段代码会抛出异常。

尽管var关键字是可接受的,但是声明类型通常被认为是个好习惯。

int i = 0;

bool is_done = false;

String muppet = 'Piggy';

Date now = new Date.now();

对于包含其他类型的类型,也可以声明对象要持有的类型。

HashMap<String,bool> is_awesome = {

  'Scooter': false,

  'Bert': true,

  'Ernie': false

};

List<int> primes = [1,2,3,5,7,11];

声明类型使代码的意图更明确也更容易阅读,这对可维护性是很重要的。在编译代码时,类型也将有助于解释器,允许它运行得更快。

本章中包含了很多方面的内容。此刻,Dart语言中的很多东西应该仍是感到熟悉的,也有一些关键但(我希望是)受欢迎的差异。

JavaScript 的一个特点是它支持函数式编程。因为 Dart 的目标是让人感觉熟悉,现在让我们看看在Dart语言中函数式编程是什么样的。

我们先从一个传统的例子开始,计算斐波纳契数列。在 JavaScript 中,大概像下面这样写:

function fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

探索一个语言的函数式编程特性,斐波纳契数列是个很好的例子。不仅因为它是一个函数,也因为它的递归性质可以展示函数的调用。

我不想纠缠于描述递归或者这个函数的细节。相反,让我们关注如何在JavaScript中使用这个函数。

【注】①斐波纳契数列在其他地方有很好的描述,如果你需要重温一下,请参考 htp:/en.wikipedia.org/wiki/Fibonacci_number。

fib(1) // => 1

fib(3) // => 2

fib(10) // => 55

看得出JavaScript函数足够简单。首先是function关键字,然后是函数名,跟着是圆括号中的参数列表,最后是描述函数体的代码块。

那么,等价的Dart语言版本是什么样呢?

// Dart

fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

等一下,这和JavaScript的版本有什么不同吗?

function fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

细心的读者会注意到,Dart版本缺少function关键字。除了这一点,两个函数是完全相同的,调用方式也一样。

fib(1); // => 1

fib(5); // => 5

fib(10); // => 55

如果没有其他区别,可以看出Dart语言的设计者确实让这门语言让人感觉很熟悉。

有经验的JavaScript程序员非常精通于使用匿名函数。因为在JavaScript中函数是顶级概念,函数可以在 JavaScript 中任意传递。甚至某些框架成了回调函数的地域,但是撇开审美不说,没有人会否认匿名函数是 JavaScript 中的一个重要部分。那么,在Dart中也一定是这样的,对吗?

在JavaScript中,匿名函数省略了函数名,仅使用function关键字。

function(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

我们已经看到JavaScript和Dart的函数仅有的差异是后者没有function关键字。事实证明,这也是二者在匿名函数上仅有的差异。

(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

乍一看,这看起来很奇怪,感觉光秃秃的。但是,这仅仅是从 JavaScript 的角度来看。Ruby中有比较类似的lambda和Proc。

{ |i| $stderr.puts i }

认真地考虑一下,在JavaScript中function关键字真正起了什么作用?下意识的反应是,它有助于标识出匿名函数,但在实践中,这仅仅是一个干扰。

考虑下面这个显示斐波纳契数值的代码:

var list = [1, 5, 8, 10];

list.forEach(function(i) {fib_printer(i)});

function fib_printer(i) {

  console.log("Fib(" + i + "): " + fib(i));

}

function fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

function关键字对代码的可读性有帮助还是有阻碍?显然,这使情况变得更糟,尤其是在foreach()调用的内部。

让我们考虑以下等价的Dart代码。

var list = [1, 5, 8, 10];

list.forEach((i) {fib_printer(i);});

fib_printer(i) {

  print("Fib($i): ${fib(i)}");

}

fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

注意:我们使用了最早在第 1 章中提到的字符串变量插值的技巧:"Fib($i):${fib(i)}"。

我们所做的只是删除了function关键字,但是代码的意图更清晰了。如果这种效果贯穿于整个项目,那么将显著提升代码库的长期健康。

说到清晰,如果你嫌大括号麻烦,对于简单的函数还有一种更酷的语法。这个迭代语句中的匿名函数(i) {fib_printer(i);}可以写成(i) => fib_printer(i)。这样,我们的代码就变成了下面这样:

var list = [1, 5, 8, 10];

list.forEach((i) => fib_printer(i));

fib_printer(i) {

  print("Fib($i): ${fib(i)}");

}

fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

参数(i)在匿名函数的定义和fib_printer(i)调用中重复出现了。在JavaScript中,没有更清晰的做法了。然而在Dart中,函数(i) => fib_printer(i)可以进一步被简化为简单的fib_printer

【注】① 在JavaScript中也是可以的:list.forEach(fib_printer);。—译者注

【注】② Dart中forEach()方法要求的参数是一个void f(E element)的函数,所以可以直接传递函数名。—译者注

var list = [1, 5, 8, 10];

list.forEach(fib_printer);

fib_printer(i) {

  print("Fib($i): ${fib(i)}");

}

fib(i) {

  if (i < 2) return i;

  return fib(i-2) + fib(i-1);

}

在这段Dart代码中,这么用确实很简短。

像前面的 forEach()方法那样,这种把匿名函数传递到迭代方法中的行为,已经展示出了函数作为头等公民的良好支持,也就是说,可以把函数像变量一样进行赋值和传递。

在写作本书时,Dart语言还缺少一些功能(例如反射)来支持复杂的函数式概念,如 curry 化或组合(combinator)。不过,在 Dart 中已经可以执行偏函数应用(partial function application)了。

【注】① Dart语言已经明确表示要支持基于镜像的反射(Mirror-based Reflection),估计很快将会实现。另外,这里说的偏函数和组合应该是已经支持了,本节中的示例代码已经可以正常运行,但在作者写作时尚不支持。—译者注

偏函数应用的典型示例是把一个对3个数字求和的函数转化为固定了其中的一个数字的函数。

add(x, y, z) {

  return x + y + z;

}

makeAdder2(fn, arg1) {

  return (y, z) {

   return fn(arg1, y, z);

 };

}

var add10 = makeAdder2(add, 10);

偏函数应用这个名字来自于返回一个已经应用了一个参数的函数。在这里, makeAdder2这个函数返回另外一个接收两个参数的函数。调用这个新函数的结果与调用第一个参数固定为arg1的原函数的结果一样。

在这里,add10()函数接收两个数字参数,对它们求和,再加上10。

在JavaScript应用中更繁琐的事情之一是提取可选参数。在Dart语言中使用内建的语法来封装这个概念,解决了这一问题。

像下面这样,把可选参数放在方括号中:

f(a, {b1:'who', b2, b3, b4, b5, b6, b7}) {

  // ...

}

可以完全不用任何可选参数来调用这个函数:f('foo')。在这种情况下,函数体中的参数a将被赋值为'foo'。

要指定可选参数,需要在函数调用中给它们加上参数名作为前缀。

f('foo', b6:'bar', b3:'baz');

调用前面这个函数的结果是,在f()方法中,变量a赋值为'foo',b1是 'who', b3是'baz',b6是'bar'。所有其他可选参数b2、b4、b5和b7都是null。

这里要特别注意的是,我们可以在函数参数列表中指定可选参数的默认值。在这个例子中,变量b1的默认值是字符串 'who'

【注】① 在 M1 版本中,可选参数分为按名使用和按位置使用的可选参数。按位置的可选参数使用方括号的形式,用等号定义参数的默认值,在使用时不能指定参数名,仅根据参数位置对应。而命名的可选参数使用花括号的形式,用冒号定义参数的默认值,在使用时必须指定参数名,所以参数名也是API的一部分。一个函数和方法只能使用其中的一种形式。详见http://www.dartlang.org/articles/m1-language-changes/中相关的部分。—译者注

通过对象字面量确实有了改进。可选参数在类和实例方法中甚至更强大,像我们将在第7章中看到的那样。

关于this的简要说明:Dart与JavaScript的不同之一是this关键字的使用方式。Dart对此的观点是,this与当前函数毫不相干。相反,this是保留给对象使用的并始终指向当前对象。在 Dart 语言中,没有对 this的绑定(binding)、应用(applying)或调用(calling)。换句话说,this与函数无关。我们将在第7章中再讨论这一点,不过会比较简短,因为它在Dart中非常简单。

本章所述内容的许多方面都还在快速演进中。Dart 语言缺少一些当前 JavaScript程序员可用的功能:没有反射,而且函数中也没有访问参数的arguments属性。

即使没有这些,我们也已经看到Dart语言在我们能做的事情上是非常强大的。我们将在第11章再次回到这个主题上。

相关图书

Rust游戏开发实战
Rust游戏开发实战
JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
Rust实战
Rust实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战

相关文章

相关课程