Dart 语言导览

来自Dart 语言
XsLiDian讨论 | 贡献2012年9月15日 (六) 15:21的版本

(差异) ←上一版本 | 已认可版本 (差异) | 最后版本 (差异) | 下一版本→ (差异)
跳转至: 导航搜索


本文译自 Dart 语言官网 A Tour of the Dart Language 一文,原作者为 Kathy & Seth。原文授权许可为 CC-BY 3.0

欢迎参观 Dart 语言! 在这里您将看到 Dart 各项主要功能的用法, 包括变量、运算符、类、库等。 本文假定您已经了解如何用其他语言编程。

提示: 如需尝试各项功能,请使用 Dart Editor 创建服务器应用项目。

如需详细了解某项语言功能,请参考 Dart 语言规范

目录

基本的 Dart 程序

以下代码 用到了 Dart 最基本的一些功能。

main() {
  // 变量及取值
  int number   = 42;
  String text  = "number 的数值为";

  // 控制台输出
  print("$text $number.");
}

下面讲解该程序中大部分 Dart 应用都能用到的部分:

main()

最特别的,必需, 应用开始执行时的顶级函数。

//

表示该行剩下的部分为#注释。 也可以这样写:

/* 可以超过一行的注释内容 */

int, String

通过静态类型说明声明变量。

"..." (或 '...')

字符串。

$变量名

字符串内插入变量的字符串值或 toString() 的返回值。

print()

显示输出的简便途径。

样式

我们的代码遵循 Dart 样式指南中的约定。 譬如说,约定缩进长度为两个空格。

运行时模式

Dart 程序可以以生产模式或强制模式运行。

生产模式(Production mode)是 Dart 程序默认的运行时模式, 为速度而优化。 生产模式下,可选的静态类型声明会被忽略。

强制模式(Checked mode)是适合开发者使用的模式, 可帮助您揪出运行过程中的一些类型错误。 例如,如果将非字符串数据赋值给声明为 String 类型的变量, 则会报告异常。

我们建议您在强制模式下开发与调试, 而在生产模式下部署。

变量

下面是创建变量并给其赋值的例子:

var name = 'Bob';

变量及引用。名为 name 的变量包含对值为“Bob”的 String 对象的引用。

默认值

未初始化的变量均有一初始值 null。包括数字类型,属性均为对象。

num lineCount;
lineCount == null; // 表达式结果为 true


可选类型

您可以选择在变量声明语句中加上静态类型:

String name = 'Bob';

添加类型名称有助于清晰地表达您的意图。 编译器和编辑器等工具可以利用这些类型声明 提供 bug 预警与代码补全功能。

final

如果您不打算改变某个变量的值,可以使用 final 代替var,或者 在类型名称前加上 final 关键字。final 变量一旦赋值,便无法再更改。

final String name = 'Bob';
name = 'Alice'; // 编译器报错 ERROR (VM 或 JavaScript)

小结

Dart 中变量类型可选,尽管我们通常推荐使用类型说明。 变量可以标记为 final 以锁定其值。 未初始化的变量均有初始值 null

内置类型

Dart 语言对以下类型提供特别支持:

您可以使用常量初始化任一特殊类型对象。 例如,'这是一个字符串' 是字符串常量, 而 true 是布尔型常量。

由于 Dart 中的所有变量均为对象—— 某的一个实例—— 您通常可以用构造函数创建变量。 对内置类型的变量也适用。 例如,您可以使用 Map() 构造函数创建映射, 代码为 new Map()

字符串

Dart 中的字符串是一组 Unicode 字符代码的序列。 您可以使用单引号或双引号创建字符串:

var s1 = '单引号可以很好地作为字符串常量。';
var s2 = "双引号的效果也是一样的。";
var s3 = '字符串分界符 \' 的转义很方便。';
var s4 = "使用双引号作为分界符更方便,不用对 ' 进行转义。";

您可以使用 ${表达式} 在字符串中嵌入表达式数值。 如果表达式是变量名称,可以省去 {}

var s = '内插字符串(string interpolation)';

print('Dart 的 $s 很方便使用。');
print('如果需要全部大写,${s.toUpperCase()} 非常方便!');

您可以将相邻的字符串常量连接为字符串:

var s = '字符串''连接'
        "甚至可以跨行进行";
print(s); // 字符串连接甚至可以跨行进行

另一种创建多行字符串的方法是 使用三个引号,单双皆可。

var s1 = '''
您可以像这样
创建多行字符串。
''';

var s2 = """我也是
多行字符串啊喂。""";

您可以在字符串开头加上 @ 创建“纯”字符串(译注:即赋值时不转义)。

var s = @'在纯字符串中,连 \n 都会被忽略。';

与其他所有对象一样,您可以使用 == 运算符检查两个字符串是否等价(字符全部相同):

var name = 'NAME';
var greeting = "Hello, $name!";
var greetingTemplate = 'Hello, NAME!';

print(greeting == greetingTemplate); // 输出 true;字符完全相同

字符串方法

所有字符串常量的类型均为 StringString 有一些实用方法,包括比较等功能。

var fullName = 'Cuthbert Musgrave Girdlestone, III';

fullName.startsWith('Cuthbert');            // true;以“Cuthbert”开头
fullName.endsWith('III');                   // true;以“III”结尾
fullName.contains(new RegExp('Musgrave'));  // true;匹配正则表达式“/Musgrave/”

字符串对象无法改变(immutable), 即只能创建,但不能修改。 如果您仔细阅读 String API 文档 会发现,所有方法都不会 真正地改变 String 类型的状态。 例如,replaceAll() 方法只返回新的 String 对象, 而没有改变原来的 String 对象。

var greetingTemplate = 'Hello, NAME!';
var greeting = greetingTemplate.replaceAll(new RegExp("NAME"), 'Bob');
print(greeting == greetingTemplate); // 输出 false;greetingTemplate 没有变化

StringBuffer 方法

要通过程序生成字符串,您可以使用 StringBufferStringBuffer 在调用 toString() 之前不会生成新的 String 对象。

var sb = new StringBuffer();

sb.add("使用 StringBuffer");
sb.addAll([" 可", "高效", "创建", "字符串,"]);
sb.add("数量越多 ").add("效果越明显。");

var fullString = sb.toString();

print(fullString); // 使用 StringBuffer 可高效创建字符串,
                   // 数量越多 效果越明显。

sb.clear();        // 清空对象!


注: 目前,编译为 JavaScript 代码后 StringBuffer 执行很慢。 详情请见 bug #1216

数字

Dart 中的数字分为两种:

int

任意大小的整数

double

十进制 64 位双精度浮点数,遵循 IEEE 754 规范所约定的格式

intdouble 都是 num 的子接口。 num 接口定义了基本的运算符,如 +-/*, 以及位运算符,如 >>

num 接口中还有 abs()ceil()floor() 等方法。 如果 num 及其子接口中没有您需要的功能, Math 类中可能会有所提供。

整数是没有小数点的数字。 下面是一些定义整数常量的例子:

var x = 1;
var hex = 0xDEADBEEF;
var bigInt = 3465346583465243765923847659234765928347659567398475647495873984572947593470294387093493456870849216348723763945678236420938467345762304958724596873045876234572037862934765294365243652548673456705673465273465246734506873456729457623845623456234650457693475603768922346728346256;

如果数字含有小数点, 则为双精度浮点数。 以下是一些定义双精度浮点数常量的例子:

var y = 1.1;
var exponents = 1.42e5;

下例展示如何在字符串和数字类型之间进行相互转换:

// string -> int
var one = Math.parseInt("1");                   // 1

// string -> double
var onePointOne = Math.parseDouble("1.1");      // 1.1

// int -> string
String oneAsString = 1.toString();              // "1"

// double -> string
String piAsString = 3.14159.toStringAsFixed(2); // "3.14"

布尔

Dart 有正式的布尔类型,名为 bool。 只有两种对象的类型为 bool: 布尔常量,truefalse

当 Dart 需要一个布尔值时, 如果该值不是 true, 那就肯定是 false。 不像在 JavaScript 中 1 或非 null 对象不作为 true 对待。

例如,对于下述的代码:

var name = 'Bob';
if (name) {
  print("你有名字耶!"); // 在 JavaScript 中可以输出,但 Dart 中不能
}

在 JavaScript 环境,该段代码会输出“你有名字耶!”,因为 name 是非 null 对象。 但在 Dart 中,这段代码不会输出任何内容, 因为 name 被作为 false 对待 (表达式 name != true 成立)。

下面是另一段 JavaScript 与 Dart 表现截然不同的示例代码:

if (1) {
  print("JavaScript 会输出本行,因为它认为 1 等于 true。");
} else {
  print("Dart 会输出本行,因为它认为 1 *不* 等于 true。");
}


注: Dart 实验室目前对上述两例的表现有误, 它会输出 JavaScript 的执行结果。 (详情请见 bug #1190。) 还需要注意的是, 与本文其余示例代码不同, 上述两例不能在强制模式下执行。

Dart 对布尔值的处理方式 是为了避免很多值被作为 true 对待 所引发的诸多奇怪现象。 对您而言, 应避免使用 if (非布尔值) 这样的代码, 而应该对值进行显式检查。 例如:

// 检查空字符串
var fullName = '';
if (fullName.isEmpty()) {
  print("请输入姓名");
}

// 检查零分
var hitPoints = 0;
if (hitPoints == 0) {
  print("啊呃!你好像挂掉了哎。");
}

// 检查 null
var unicorn = null;
if (unicorn == null) {
  print("许愿不够给力。再加点油!");
}

// 检查 NaN
var iMeantToDoThis = 0/0;
if (iMeantToDoThis.isNaN()) {
  print("0/0 不是数字。");
}

列表(即数组)

几乎所有编程语言中都有的最常见的集合或许就是 数组 了——或者称之为有顺序的对象集合。 在 Dart 中,数组属于 List 类型的对象, 因此我们通常称之为 列表。 当您将 Dart 编译为 JavaScript 脚本时, Dart 列表会被编译为 JavaScript 数组。

Dart 列表常量与 JavaScript 数组常量一样。 这是一个简单的 Dart 列表:

var list = [1,2,3];

您可以获取列表的长度 以及引用列表元素, 语法与 JavaScript 相同:

var list = [1,2,3];
print(list.length); // 元素数目:3
print(list[1]);     // 第二项:2

您可以使用 add() 方法将元素添加到列表:

var list = [1,2,3];
list.add(4);

要将元素从列表移除 (减短列表长度), 请使用 removeRange() 方法:

var list = [1,2,3,4];
list.removeRange(2, 1); // 移除第三个元素

遍历

如果需要处理列表的每一个元素, 您可以使用 forfor...inforEach()。 如果需要当前遍历到的索引编号,请使用 for 语句:

var list = [1,2,3];
for (var x = 0; x < list.length; x++) {
  print('$x: ${list[x]}');
}

如果不需要索引编号, 可以使用 for...in 语句:

var list = [1,2,3];
for (final x in list) {
  print(x);
}

如果只是想对列表各元素应用某个函数, 请使用 forEach() 方法:

var list = [1,2,3];
void printElement(element) => print(element);
list.forEach(printElement);

或者更简洁地:

var list = [1,2,3];
list.forEach((element) => print(element));

列表与集合方法

forEach() 方法 是 List 接口及其超接口 Collection 所定义的诸多实用方法之一。 其他还有如: filter() 方法 可返回新的集合,只包含满足特定条件的元素。 every()some() 方法 分别可用来检查集合是否匹配 所有条件或至少一个条件。 sort() 方法 允许按任意需要的条件对列表进行排序。

关于列表的更多信息请见 #泛型

映射

总的来说,映射是包含键值对应关系的对象。 Dart 对映射的支持是通过映射常量以及 Map 接口实现的。

下面是简单 Dart 映射的例子:

var gifts = {     // 映射常量
// 键       值
  "第一天": "山鹑",
  "第二天": "斑鸠",
  "第五天": "环颈雉"};

在映射常量中,每个 必须为字符串。 如果您使用 Map 的构造函数, 还有其他选择: 键可以是字符串、数字或其他任意实现了 Hashable 接口的对象。

var map = new Map();   // 使用 Map 构造函数
map[1] = "山鹑";       // 键为 1;值为“山鹑”
map[2] = "斑鸠";       // 键为 2;值为“斑鸠”
map[5] = "环颈雉";     // 键为 5;值为“环颈雉”

映射中的 可以是任意对象或 null

将新的键值对添加到现有映射中的方法 与 JavaScript 中无异:

var gifts = { "第一天": "山鹑" };
gifts["第四天"] = "乌鸫";   // 添加一组键值对

从映射中提取值的方法也和 JavaScript 中一样:

var gifts = { "第一天": "山鹑" };
print(gifts['第一天']);        // 山鹑

如果您查找的键在映射中不存在, 将在返回中得到 null。 不过,由于值可以为 null, 您可能需要使用 containsKey()putIfAbsent() 等方法获知 null 的确切情况。

var gifts = { "第一天": "山鹑" };
print(gifts['第五天']);        // null

使用 .length 可以获取映射中键值对的数目:

var gifts = { "第一天": "山鹑" };
gifts["第四天"] = "乌鸫";
print(gifts.length);         // 2

要从映射中移除键值对, 可以使用 remove() 方法:

var gifts = { "第一天": "山鹑" };
gifts["第四天"] = "乌鸫";
gifts.remove('第一天');
print(gifts.length);           // 1
print(gifts['第一天']);        // null

您可以使用 Map.from() 构造函数复制映射:

var gifts = { "第一天": "山鹑" };
var regifts = new Map.from(gifts);
print(regifts['第一天']);      // 山鹑

遍历

遍历映射的内容有几种方法。 使用 forEach() 方法 可同时访问键值。

var gifts = {
  "第一天": "山鹑",
  "第二天": "斑鸠",
  "第五天": "环颈雉"};
gifts.forEach((k,v) => print('$k:$v'));

注: 不要指望 forEach() 按特定顺序返回键值对。

如果您只对键或值感兴趣, 可以分别使用 getKeys()getValues()。 两种方法均可返回 Collection 对象。

var gifts = {"第一天": "山鹑", "第二天": "斑鸠"};
var values = gifts.getValues();
values.forEach((v) => print(v));      // 山鹑, 斑鸠

注: 映射对象本身不会扩展到 Collection 接口。

内置类型小结

Dart 的 #内置类型 均有 特别的常量格式,并实现了内置接口。 例如,数字常量如 11.1, 它们实现了 num 接口。

您通常会用常量创建大多数内置类型对象, 但也可以用构造函数。 布尔类型有所不同,因为您无法创建类型为 bool 的新对象; 只能用 truefalse 实现。

关于映射与列表的详细信息, 请参见 #泛型

函数

下面是一个简单的函数:

String say(String from, String msg) => "$from 说“$msg”";

下面是调用该函数的示例:

print(say("Bob", "Hello")); // "Bob 说“Hello”"

若忽略类型,上面的代码也可以这样写:

say(from, msg) => "$from 说“$msg”";

不过,我们推荐在函数签名处说明参数类型。

=> e; 语法是 { return e; } 的简写式。 如 say(from, msg) => "$from 说“$msg”"; 与下面的写法等效:

say(from, msg) {
  return "$from 说“$msg”";
}

可选参数

将函数参数放在 [] 括号内可将其标记为可选参数。

String say(String from, String msg, [String device]) {
  var result = "$from 说“$msg”";
  if (device != null) {
    result = "$result(通过 $device 发送)";
  }
  return result;
}

下面是调用时不提供可选参数的例子:

print(say("Bob", "你好呀")); // Bob 说“你好呀”

下面是提供第三个参数调用该函数时的例子:

print(say("Bob", "你好呀", "狼烟"));
// Bob 说“你好呀”(通过 狼烟 发送)

可选参数的默认值

您可以给可选参数指定默认值。默认值必须为编译时常量。 如果没有提供默认值,其值则为 null(上例即是)。

String say(String from, String msg, [String device='信鸽']) {
  var result = "$from 说“$msg”";
  if (device != null) {
    result = "$result(通过 $device 发送)";
  }
  return result;
}

如果省去可选参数,则会使用默认值:

print(say("Bob", "你好呀")); // Bob 说“你好呀”(通过 信鸽 发送)

命名参数

可选参数同时也是命名参数。

print(say("Bob", "你好呀", device: "易拉罐话筒"));
// Bob 说“你好呀”(通过 易拉罐话筒 发送)

第一类函数

您可以将函数作为参数传递给其他函数。例如:

List ages = [1,4,5,7,10,14,21];
List oddAges = ages.filter((i) => i % 2 == 1);

与下面的代码等效:

bool isOdd(num i) => i % 2 == 1;
List ages = [1,4,5,7,10,14,21];
List oddAges = ages.filter(isOdd);

您也可以将函数赋值给变量,如:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
print(loudify('hello'));

词法封装

函数可将周边区域定义的变量进行封装处理。 下例展示 makeAdder 函数如何捕获变量 n 并将其传递给 makeAdder 所返回的函数。 不论所返回的函数在哪里调用,它都能捕获变量 n

Function makeAdder(num n) {
  return (num i) => n + i;
}

main() {
  var add2 = makeAdder(2);
  print(add2(3)); // 5
}

(特别感谢 Bob Nystrom 提供本例。)

返回值

所有函数都会返回一个值。如果未指定返回值,则将在函数主体的末端隐式调用 return null; 语句。

函数小结

Dart 支持第一类函数,以及可选参数、命名参数和参数默认值。 函数可被赋值给变量,或作为参数传递给其他函数。 函数还支持词法封装,可访问其直接词域以外的变量。

运算符

Dart 支持运算符。 它不仅定义运算符, 还允许您重新定义其中很多运算符。

下表按优先级顺序 列出了 Dart 的全部运算符。

描述 运算符 结合性
一元后缀 表达式++ 表达式-- () [] .
一元前缀 -表达式 !表达式 ~表达式 ++表达式 --表达式
乘除 * / % ~/
加减 + -
移位 << >>
关系 is is! >= > <= <
相等性 == != === !==
位 AND &
位 XOR ^
位 OR |
逻辑 AND &&
逻辑 OR ||
条件 表达式 ? 表达式 : 表达式
赋值 = *= /= ~/= %= += -= <<= >>= &= ^= |=


例如, % 运算符的优先级 高于 == 运算符,因此在其之前执行; 而 == 的优先级又高于 && 运算符。 这种优先顺序意味着 下面两行代码是等价执行的:

if ((n % i == 0) && (d % i == 0)) // 括号可增强可读性
if (n % i == 0 && d % i == 0)     // 难以阅读,但与上一行等价

本段落将涉及下述主题:

算术运算符

Dart 支持常用的算术运算符。

运算符 定义
+
-表达式 一元否定(令表达式符号取反)
*
/
~/ 除,返回结果的整数部分
% 求余

例:

int a = 2;
int b = 3;

print('${a + b}');  // 5
print('${a - b}');  // -1
print('${a * b}');  // 6
print('${a / b}');  // 0.6666666666666666
print('${a ~/ b}'); // 0 (商)
print('${a % b}');  // 2 (余数)

Dart 还支持前后缀自增减运算符。

运算符 定义
++变量 变量 = 变量 + 1 (表达式值为 变量 + 1
变量++ 变量 = 变量 + 1 (表达式值为 变量
--变量 变量 = 变量 – 1 (表达式值为 变量 – 1
变量-- 变量 = 变量 – 1 (表达式值为 变量

例:

int a = 2;

print('${ ++a }'); // 3 (先自增后返回)
print('${ a++ }'); // 3 (先返回后自增)
print('${ a-- }'); // 4 (先返回后自减)
print('${ --a }'); // 2 (先自减后返回)

相等性与关系运算符

运算符 定义
== 相等 (参见下文讨论)
!= 不等
=== 实例相同
!== 实例不同
> 大于
< 小于
>= 大于或等于
<= 小于或等于
is 如果对象为指定类型则为 true (参见下文讨论)
is! 如果对象为指定类型则为 false

要测试 xy 两个对象是否代表相同的事物,请使用 == 运算符。 您通常不必使用 === 运算符,该运算符用于测试两个对象是否是完全一样的对象。 == 运算符的工作原理如下:

  1. 如果 x===y,返回 true
  2. 否则,如果 xynull,则返回 false
  3. 否则,返回表达式 x.equals(y) 的结果。

isis! 运算符可方便检查类型。 如果 对象 实现了 T 所指定的接口, 则 对象 is T 的结果为 true。 例如,对象 is Object 总为 true

下面的例子用到了各个相等性与关系运算符:

int a = 2;
int b = 3;
int c = a;

print(a == 2);       // true;2 与 2 相等
print(a != b);       // true;2 与 3 不等
print(a === c);      // true;a 与 c 是完全相同的对象
print(a !== b);      // true;2 与 3 不是相同的对象
print(b > a);        // true;3 大于 2
print(a < b);        // true;2 小于 3
print(b >= b);       // true;3 大于或等于 3
print(a <= b);       // true;2 小于或等于 3
print(a is num);     // true;2 是数字类型
print(a is! String); // true;2 是 int 而非字符串类型

赋值运算符

您可以使用 = 运算符进行赋值。 也可以使用组合赋值运算符, 它们是运算符与赋值操作的结合。

组合赋值符 等效表达式
对于运算符 op a op= b a = a op b
例: a += b a = a + b

下面是赋值运算符总表:

=
+=
–=
*=
/=
~/=
%=
<<=
>>=
&=
^=
|=

下面的例子同时用到了赋值符和组合赋值运算符:

int a = 2;           // 使用 = 赋值

a *= 3;              // 相乘并赋值:a = a * 3
print('a *= 3: $a'); // 输出 a *= 3: 6

逻辑运算符

您可以使用逻辑运算符对布尔表达式进行取反或组合。

运算符 定义
!表达式 反转以下表达式 (false 变为 true,反之亦然)
|| 逻辑 OR
&& 逻辑 AND
if (!done && (col == 0 || col == 3)) {
  // ...执行一些操作
}

位操作与移位运算符

在 Dart 中,您可以操作对象各位。 这些运算符通常应与整数搭配使用。

运算符 定义
& AND
| OR
^ XOR
~表达式 一元位取补(0110
<< 左移
>> 右移

下例使用了位操作与移位运算符。

int value = 0x22;
int bitmask = 0x0F;

print(value);                                // 34 (0x22)
print(value.toRadixString(16));              // 22
print(value.toRadixString(2));               // 100010
print((value & bitmask).toRadixString(16));  // 2  (AND)
print((value & ~bitmask).toRadixString(16)); // 20 (AND NOT)
print((value | bitmask).toRadixString(16));  // 2f (OR)
print((value ^ bitmask).toRadixString(16));  // 2d (XOR)
print((value << 4).toRadixString(16));       // 220
print((value >> 4).toRadixString(16));       // 2

其他运算符

运算符 名称 定义
() 函数应用 代表函数调用
[] 列表访问 代表列表特定索引位置的值
表达式1 ? 表达式2 : 表达式3 条件 如果 表达式1true,则执行 expr2

否则,执行 表达式3

(技术上讲属于特别语法,不是运算符)
. 成员访问 代表表达式的一个属性; 例:foo.bar 从表达式 foo 中选取属性 bar

运算符的方法本质

运算符只是名字比较特别的实例方法。 例如,表达式 1 + 2 调用了 1+ 方法,参数为 2—即 1.+(2)。 这种模型有重要意义:

  • Dart 允许您重载很多运算符。 例如,如果您定义了 Vector 类, 可以再定义一个 + 方法,用来求两个向量之和。
  • 对于二元运算符, 左侧的运算元决定了 应选用运算符的版本。 例如,如果您定义了 Vector 类与 Point 类, aVector + aPoint 中会使用 Vector 版本的 +

以下运算符可以重载:

<
>
<=
>=

+
/
~/
*
%
|
^
&
<<
>>
[]

[]= (列表赋值)
~
equals() (==) *

* 目前 == 运算符可以重载,但不会一直这样。 不久以后,自定义 == 动作的途径 将是重载 equals() 方法。

关于重载运算符的例子,请参见《类》一节的运算符段落。

运算符小结

Dart 运算符的外观与行为应该不令人感到陌生。 从技术层面来看,运算符是特别命名的方法, 在首个操作数处调用。 因此,操作数的顺序可能会造成差异: a+b 的结果可能与 b+a 的不尽相同。 您可以重载很多运算符。

流程控制

您可以使用下述语句控制 Dart 代码的执行流程:

if 与 else

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

请记住,与 JavaScript 不同,Dart 将所有 非 true 的值当作 false 对待。 详情请参见布尔值

for 循环

您可以使用标准 for 循环进行遍历。

for (int i = 0; i < candidates.length; i++) {
  candidates[i].interview();
}

Dart 中 for 循环的退出可以正确捕获索引值, 没有 JavaScript 中讨厌的设计缺陷。 例如可以这样:

main() {
  var callbacks = [];
  for (var i = 0; i < 2; i++) {
    callbacks.add(() => print(i));
  }
  callbacks.forEach((c) => c());
}

输出内容是 01,与预期的一样。 相反,在 JavaScript 中,该例会输出 22

如果您要遍历的对象是 Collection 类型, 可以使用 forEach() 方法。如果不需要知道当前遍历的位置, 使用 forEach() 是个不错的选择。

candidates.forEach((candidate) => candidate.interview());

集合还支持 for-in 形式的遍历:

var collection = [0, 1, 2];
for (var x in collection) {
  print(x);
}
// 输出内容:
// 0
// 1
// 2

while 与 do while

while 循环语句可在循环前判断条件是否成立。

while (!auctionItem.currentWinner(bidder) &&
       auctionItem.currentBid < bidder.maximumBid) {
  auctionItem.placeBid(bidder, auction.currentBid + 1);
}

do while 循环可在循环之后判断条件是否成立。

do {
  printLine();
} while (!atEndOfPage());

break 与 continue

使用 break 停止循环。

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

使用 continue 跳到下一次循环遍历。

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

如果您在使用 Collection,该例还可以这样写:

candidates.filter((c) => c.yearsExperience >= 5)
          .forEach((c) => c.interview());

switch 与 case

Dart 中的 switch 语句使用 == 比较对象。记得在每条非空 case 分句后加上 break 语句,以避免跳到下一句(属于错误,参见下文)。 default 分句可用于捕获无匹配的条件。

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClose();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

下例的 case 分句中省略了 break 语句, 会引发报错:

var command = 'OPEN';
switch (command) {

  case 'OPEN':
    executeOpen();
    // 错误:缺少 break 会触发异常!!

  case 'CLOSED':
    executeClose();
    break;
}

不过 Dart 的确支持空的 case 分句, 允许跳到下一分句。

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':     // 空分句会跳到下一分句
  case 'NOW_CLOSED':
    // CLOSED 与 NOW_CLOSED 均执行这段代码
    executeClose();
    break;
}

异常处理

Dart 代码可以抛出与捕捉异常。 异常是预期以外情况发生的错误信号。 如果没有捕捉, 异常将不断返回至程序顶层。

与 Java 不同的是,Dart 的所有异常都是非强制异常。 方法不会声明可能抛出的异常, 您也不必捕捉所有异常。

Dart 提供了 Exception 接口以及很多预定义的异常类型。 当然,您可以通过扩展 Exception 接口定义自己的异常类型。 常见异常的例子包括:

不过,Dart 程序可以将任意对象作为异常抛出。

抛出

您可以这样抛出——或者说 引发——异常。

throw new IllegalArgumentException('数值必须大于零');

您也可以抛出任意对象。

throw "骆马不够用了!";

捕捉

捕捉——或者说捕获——异常,可避免该异常向上级扩散。 捕捉为处理异常提供了可能。

try {
  breedMoreLlamas();
} catch (final OutOfLlamasException e) {
  buyMoreLlamas();
}

若要处理抛出多种类型异常的代码,您可以指定多条捕捉分句。 第一条捕捉分句匹配抛出对象的类型, 负责处理异常。 如果捕捉分句未指定类型,该分句将能处理任意类型的抛出对象。

try {
  breedMoreLlamas();
} catch (final OutOfLlamasException e) {  // 特定异常
  buyMoreLlamas();
} catch (final Exception e) {             // 任意异常
  print("Unknown exception: $e");
} catch (final e) {                       // 类型不确定,全部处理
  print("Something really unknown: $e");
}

finally

为确保不论是否有异常抛出,仍有一部分代码能够执行, 请使用 finally 分句。

如果没有捕捉分句与异常相匹配, 将执行 finally 分句,而异常也将不断扩散。

try {
  breedMoreLlamas();
} finally {
  cleanLlamaStalls();  // 总会执行,即使有异常抛出
}

finally 分句也会在匹配到任意一条捕捉分句后执行。

try {
  breedMoreLlamas();
} catch (final e) {
  print("Error: $e");  // 先处理异常
} finally {
  cleanLlamaStalls();  // 然后执行 finally 分句
}


Dart 是一款面向对象的语言,支持类与单继承。 根类为 Object

实例变量

您可以这样通过实例变量 (或称成员变量) 声明一个类:

class Point {
  num x, y;
}

所有未初始化的实例变量都有默认值 null

不论实例变量是否为 final 变量, 均隐式包含了 getter 方法。 非 final 实例变量 隐式包含了 setter 方法。 (gettersetter 将在下文深入讨论。)

main() {
  var point = new Point();

  // 使用 x 的 setter 方法
  point.x = 4;

  // 使用 x 的 getter 方法
  print(point.x);  // 4

  // 默认值为 null
  print(point.y);  // null
}

实例变量的初始化

如果您在声明 (而非通过构造函数或方法) 实例变量时对其进行初始化, 初始值必须为编译时常量。

注:此限制目前尚在讨论当中。

编译时常量的例子如数字常量:

class Point {
  num x = 0,
      y = 0;
}

要给实例变量赋非常量值, 请使用构造函数体(将在下一段落讲解)。

构造函数

您可以通过创建与类同名的方法来声明构造函数。 最常见的构造函数形式是 生成构造函数, 可创建该类的新实例。

class Point {
  num x, y;

  Point(num x, num y) {
    // 还有更好的方法实现,敬请期待
    this.x = x;
    this.y = y;
  }
}

关键词 this 可引用当前实例。

注:仅当存在名字冲突时需要使用 this。其余情况下,Dart 的编写风格要求省略 this

将构造函数参数赋值给成员变量的语句特别常用, 因此 Dart 提供了简化代码的糖衣语法。

class Point {
  num x, y;

  // this.x = x 与 this.y = y 的糖衣语法
  Point(this.x, this.y);
}

默认构造函数

如果您不声明构造函数, 将会调用默认构造函数。 默认构造函数没有参数, 调用的是超类的无参构造函数。

初始化列表

final 变量必须在对象赋值给 this 之前初始化。 您可以使用先于构造函数体执行的初始化列表 初始化任意 final 变量。

#import('dart:html');

class Button {
  final Collection<ButtonHandler> handlers;
  final String domId;
  final Element elem;
  
                // : 以后便是初始化列表
  Button(domId) : domId = domId,
                  handlers = [],
                  elem = document.query(domId) {
    bindHandlers();
  }

  bindHandlers() {
   // ...
  }
}

初始化语句的右侧无权访问 this

命名构造函数

您可以使用命名构造函数实现同一个类的多个构造函数 或让代码更为清晰。

class Point {
  num x, y;

  // 命名构造函数
  Point.fromJson(Map json) : x = json['x'], y = json['y'];

  Point(this.x, this.y);
}

通过 new 创建命名构造函数的新实例:

var jsonData = JSON.parse('{"x":1, "y":2}');
var point = new Point.fromJson(jsonData);

常量构造函数

Dart 的对象创建非常确定, 因此避免了其他语言中的棘手问题。 为实现这种确定性,Dart 中只允许将固定的编译时表达式 作为实例变量声明的初始值。

固定的编译时常量对象也称为 const 对象。 要创建 const 对象,需要定义 const 构造函数,并确保所有实例字段均有 final 关键字。

class Point {
  final num x, y;
  const Point(this.x, this.y);
  static final Point origin = const Point(0, 0);
}

由于编译时常量是常量且固定, 构造两个一样的 const 对象 会生成完全一样的实例。

void main() {
  var a = const Point(1, 1);
  var b = const Point(1, 1);

  print(a === b); // 输出 true,两个实例完全一样!
}

注:其他运行时常量的例子如 数字常量与字符串常量。

factory 构造函数

要实现不总是创建类的新实例的构造函数, 可使用 factory 关键字。 例如,factory 构造函数 可能会返回缓存中的实例, 或是可能返回子类的实例。

下例演示了返回缓存中对象的 factory 构造函数。

class Logger {
  final String name;
  bool mute = false;

  static Map<String, Logger> _cache;

  factory Logger(String name) {
    if (_cache == null) {
      _cache = {};
    }

    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = new Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  log(String msg) {
    if (!mute) {
      print(msg);
    }
  }
}

对其他构造函数, 如需调用 factory 构造函数,您可以使用 new 关键字:

var logger = new Logger('UI');
logger.log('按钮被点击了');

注:factory 构造函数无权访问 this

方法

方法是提供对象行为的函数。

实例方法

对象的实例方法可以访问实例变量与 this, 下例中的 distanceTo() 方法便是实例方法的一个例子。

class Point {
  num x, y;
  Point(this.x, this.y);

  num distanceTo(Point other) {
    return Math.sqrt(((x-other.x)*(x-other.x)) + ((y-other.y)*(y-other.y)));
  }
}

您可以这样调用 Point 实例的 distanceTo() 方法:

var point = new Point(2, 2);
num distance = point.distanceTo(new Point(4,4));
print(distance);  // 2.82842...

gettersetter

gettersetter 方法提供了对象内部状态的读写权限。 调用 gettersetter 时, 请省略后面的圆括号。 您可以使用 getset 关键字定义 gettersetter

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  num get right()           => left + width;
      set right(num value)  => left = value - width;
  num get bottom()          => top + height;
      set bottom(num value) => top = value - height;
}


gettersetter 的调用 与使用实例变量所生成的 gettersetter 无异。

var rect = new Rectangle(3, 4, 20, 15);
print(rect.left); // 3
rect.right = 12;
print(rect.left); // -8

有了 gettersetter, 您可以将实例变量封装成方法, 而不必修改客户端代码。

运算符

由于运算符只是名称特别的实例方法, 您可以重载很多运算符。 下例中的类重载了 +- 运算符。

class Vector {
  final int x,y;
  const Vector(this.x, this.y);

  Vector operator +(Vector v) { // 重载 + (a + b)
    return new Vector(x + v.x, y + v.y);
  }
  
  Vector operator -(Vector v) { // 重载 - (a - b)
    return new Vector(x - v.x, y - v.y);
  }
  
  Vector operator negate() {    // 重载一元取反运算符 (-a)
    return new Vector(-x,-y);
  }
  
  String toString() => '($x,$y)';
}

main() {
  Vector v = new Vector(2,3);
  Vector w = new Vector(2,2);
  print(v);   // (2,3)
  print(-v);  // (-2,-3)
  print(v+w); // (4,5)
  print(v-w); // (0,1)
}


实作时请注意: 重载 - 运算符只会影响二元的减法运算。 如需重载一元形式的 -, 您必须使用特别的标识符 negate

抽象类

Dart 即将支持抽象类与抽象方法。

至 2012 年 4 月 4 日,抽象类尚未实现。 请关注 bug 1603 与 bug 1605 以跟踪开发进展。

扩展类

使用 extends 关键字可创建子类,super 关键词可引用其超类。

class Television {
  turnOn() {
    _illuminateDisplay();
    _activeIrSensor();
  }
}

class SmartTelevision extends Television {
  turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
}

子类可重载实例的方法、gettersetter

类级静态成员

使用 static 关键字可实现类级别的变量与方法。

静态方法

静态方法(类级方法)不操作实例, 因此无权访问 this

class Point {
  num x, y;
  Point(this.x, this.y);

  static num distanceBetween(Point a, Point b) {
    return Math.sqrt(((a.x-b.x)*(a.x-b.x)) + ((a.y-b.y)*(a.y-b.y)));
  }
}

main() {
  var a = new Point(2, 2);
  var b = new Point(4, 4);
  print(Point.distanceBetween(a, b));  // 2.82842...
}

最佳实践:对于常用或广泛使用的功能和函数, 请考虑使用顶级函数

替代静态方法。

静态变量

静态变量(类级变量)对处理类级状态与常量非常有用。

class Color {
  static final RED = const Color('红色');
  final String name;
  const Color(this.name);
  String toString() => name;
}

main() {
  print(Color.RED); // '红色'
}

接口

接口是定义与对象交互方式的类型。 接口可以指定类型、 构造函数、 实例变量 (或者更精确地,gettersetter) 以及超接口。 不过接口不能指定方法与构造函数当中的代码。

接口在指定 API 但不详细指定 API 如何实现时很方便。

定义接口

您可以使用 interface 关键词定义接口。 例如,下面的代码定义了 Hashable 接口:

interface Hashable {
  int hashCode();
}

注意定义的格式,接口没有方法体({...}), 只有一个分号(;)。

接口的实现

类可以通过在 implements 分句中声明来实现一或多个接口, 并提供接口所需的 API。 例如:

class Point implements Hashable {
  num x, y;
  ...
  // Hashable 所需的 API
  int hashCode() {
    int result = 17;
    result = 37 * result + x.hashCode();
    result = 37 * result + y.hashCode();
    return result;
  }
  ...
}

下例指定了 一个实现多个接口的类:

class Point implements Comparable, Hashable {
  ...
}

注: 不久后您将能够将 Dart 类作为接口对待。 该功能在创建虚对象测试时会很有用。

扩展接口

您可以创建构建在一或多个接口之上的接口。 新接口称为子接口, 所继承的全部接口均为其超接口

使用 extends 关键词 可指定作为基础的一或多个接口。 下面是创建 Hashable 子接口的例子:

interface HashablePoint extends Hashable {
  num x, y;
}

注: 技术上而言,接口没有 xy 等实例变量。 看起来是实例变量的部分,实际上是 声明 gettersetter 方法的快捷方式。

下面是实现子接口 并检查类型的例子:

class Point implements HashablePoint {
  num x, y; // HashablePoint 所需

  // Hashable 所需
  int hashCode() {
    ...
  }
}

void main() {
  Point p = new Point();
  print(p is Point);          // true
  print(p is Hashable);       // true
  print(p is HashablePoint);  // true
}

定义构造函数与默认类

接口只要指定了默认类, 就可以定义构造函数。

注: 很多 Dart API 是作为有默认类的接口实现的。 如所有内置类型, 包括 numint 均为接口。

使用 default 关键字可指定 默认类。 例如:

interface ObjectCache default MemoryCache {
  ObjectCache();
  ...
}

默认类的代码可以是这样:

class MemoryCache implements ObjectCache {
  ...
}

通过接口调用构造函数 会导致接口默认类的等效构造函数 被调用。 例如:

var cache = new ObjectCache(); // 与 new MemoryCache() 相同

接口小结

Dart 中到处都是接口, 包括内置类型与 Dart 标准库中所定义的很多类型。 由于 Dart 接口可以有默认类, 您可以使用接口构造函数。

泛型

如果您仔细阅读了 API 文档中基本数组类型 List 的相关内容, 会发现其类型实际上是 List<E><...> 符号将 List 标记为 泛型 (或称 参数化类型)—一种可以声明 常规类型参数的类型。

为何要使用泛型?

由于 Dart 中类型可选, 您不会遇到必须使用泛型的情况。 不过有时您可能会想用泛型 或其他类型: 类型(不论是否为泛型)有助于代码文档及注释的编写, 能简便地表达您的意图。

例如, 如果您打算让列表只包含字符串, 可以将其定义为 List<String> (读作“List of String”——字符串的列表)。 这样您和其他程序员,以及您的工具 (如 Dart Editor 与强制模式下的 Dart VM) 将能侦测到,将非字符串量赋值给列表 可能有误。

List<String> names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
...
names.add(42); // 强制模式下会失败(生产模式下可以通过)

另一个使用泛型的理由是 减少代码的重复量。 泛型允许您将单个接口及实现 在多种类型间共享, 同时仍保有强制模式的优势 并进行静态分析预警。 例如, 假设您创建了用于缓存对象的接口:

interface ObjectCache {
  Object getByKey(String key);
  setByKey(String key, Object value);
}

您发现需要该接口的字符串专用版本, 因此您又创建了另一个接口:

interface StringCache {
  String getByKey(String key);
  setByKey(String key, String value);
}

在此之后,您又决定想要该接口的数字专用版本…… 后续情况你懂的。

泛型可以免除挨个创建这些接口的麻烦。 您只需创建单个接口,并读入类型参数:

interface Cache<T> {
  T getByKey(String key);
  setByKey(String key, T value);
}

这段代码中的 T 是替代类型。 可以看作开发者可能会特别定义的占位符。

使用集合常量

内置的两种集合类型—— 列表映射 ——均已参数化。 参数化的常量与之前见过的常量几乎没有区别, 只需要在左括号前加上 <type>。 例如:

List<String> names = <String>['Seth', 'Kathy', 'Lars'];
Map<String, String> pages = <String>{ // 指定的类型:String
    'index.html':'主页',  // (也隐含了类型 String)
    'robots.txt':'给网络蜘蛛看的',
    'humans.txt':'咱是人类,不是机器' };

注: 映射常量的总是字符串类型。 花括号前的类型指定的是映射的类型。

使用构造函数

使用构造函数时,若要指定一或多个类型, 请将类型放在类名或接口名后面的尖括号 (<...>)中。 例如:

List<String> names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
Set<String> nameSet = new Set<String>.from(names);

下面的代码可创建 键为整数、值为 View 类型的映射:

Map<int, View> views = new Map<int, View>();

泛集合及其包含的类型

Dart 中的泛型已经过具体化—— 它们会在运行时携带类型信息。 例如,即使是在生产模式下, 您也可以测试集合类型:

List<String> names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>);           // true

不过 is 表达式 只能检查集合的类型—而不是 其内部的对象。 在生产模式中, List<String> 可以包含一些非字符串项。 这时的处理方案是 检查各项的类型 或将项目操作代码包含在异常处理语句中。

注: 与此相反, Java 中的泛型使用了 erasure 类型, 也就是说泛型参数在运行时会被移除。 Java 中可以测试对象是不是 List, 但不能测试它是不是 List<String>

泛型小结

关于泛型的详细信息请参见《Dart 中的可选类型》一文。

库与可访问性

本章讲解如何使用 导入#import)、 包含#source)以及 新库#library) 指令, 可帮助您 创建模块化且可共享的代码库。 库的作用不仅是提供 API, 还是隐私单位: 它们可以隐藏实现的细节,如私有变量。

每个 Dart 应用都是一个库, 即使它没有使用 #library 指令。

注: Dart 中的库体系 将有所变动。 本章描述的是目前的工作方式。

使用库

使用 #import 可指定将一个库的命名空间 用在另一个库的作用域内。

Dart web 应用通常会用到 dart:html 库,可以这样导入:

#import('dart:html');

#import 所需的唯一参数是指向库的 URI。 对于内置库, URI 以特别的 dart: 前缀区别。 对于其他库, URI 相对于当前目录。 (未来还将允许指定 web 服务器上的文件。) 例如:

#import('dart:io');
#import('mylib/mylib.dart');
#import('../../util/utilslib.dart');

指定前缀

如果您导入的两个库的标识符有冲突, 可以为其中一个或两者都指定前缀。 例如,如果 library1library2 均定义了 Element, 可以这样编写代码:

#import('lib1/library1.dart');
#import('lib2/library2.dart', prefix:'lib2');
...
var element1 = new Element();      // 使用 library1 中的 Element
var element2 = new lib2.Element(); // 使用 library2 中的 Element

库的实现

使用 #source (亦称 包含指令) 可指定属于当前库的文件, 而 #library 可指定文件实现了一个库。

将文件与当前库关联

您可以将库的实现放在多个 .dart 文件中, 并在该库的主文件中使用 #source 关联。

被包含的文件不能含有指令。 库中必须有一个文件 包含库所需的所有指令 (如 #import#library)。

下面是一个例子:

// 文件 Ballgame.dart 中的内容
#import('dart:html');

#source('Ball.dart');
#source('Util.dart');
...
main() {
  ...
}

该例中,Ball.dart 与 Util.dart 会被编译进 Ballgame.dart 这个库当中。 Ballgame.dart 没有 #library 语句, 但请记住,每个 Dart 应用都是隐式的库。

声明库

要明确声明一个库,可以使用 #library 语句。 例如:

#library('slider_sample');           // 声明这是一个库。

#import('dart:html');                // 该库用到了 html 库...
#import('../ui_lib/view/view.dart'); // ...和 view 库。

#source('SliderSample.dart');        // 从 SliderSample.dart 中获取代码。

注: #library 语句中的字符串目前用不到。

声明私有成员

如果标识符以下划线开头, 便是所属库的私有成员。 例如, 在下面的代码中,Incrementer 类 有一个名为 _hiddenNum 的实例变量。

#library('HidingLibrary');

class Incrementer {
  num _hiddenNum = 0;
  
  void incrementNum() {
    _hiddenNum++;
  }
  
  void printNum() {
    print("隐含的数字为 $_hiddenNum");
  }
}

void main() {
  var o = new Incrementer();
  print("在这里可以读取到 _hiddenNum (${o._hiddenNum})。");
}

main() 方法可以引用 _hiddenNum 因为它与 Incrementer 处于同一个库当中。 但在该库以外,_hiddenNum 将不可访问, 即便是在 Incrementer 的子类中。

例如,下面的代码无法使用 _hiddenNum 因为与 Incrementer 分别处于不同的库当中 (在库 HidingLib/hider.dart 中实现):

#library('SubclassingLibrary');
#import('../HidingLib/hider.dart');

// 此类在 Incrementer 所处库之外,因此无法使用 _hiddenNum
class DoubleIncrementer extends Incrementer {
  void incrementNum() {
    //_hiddenNum = _hiddenNum + 2; // 无法解析 _hiddenNum
    super.incrementNum();          // 只能这样令 _hiddenNum 自增
    super.incrementNum();
  }
}

库与可见性小结

如果需要使用库,请用 #import;如果需要将文件拉取到库中,请用 #source;如果需要声明您正在实现一个库, 请用 #library。 以下划线(_)命名的成员是库的私有成员。

隔离

Dart 中的所有代码都在隔离的上下文中执行。 您可以使用额外的隔离进行并发编程, 以及更安全地执行第三方代码。

隔离概念

每个隔离都有自己的 heap,即它在内存中的全部值, 包括全局变量,只能在隔离内部使用。 在隔离之间通讯的唯一途径是传递消息。 消息通过端口发送。

消息的内容可以是:

  • 基本值(nullnumbooldoubleString
  • SendPort 的实例
  • 列表或映射,其元素为上述任意类型,或是其他列表和映射
  • 特殊情况下可发送任意类型的对象

列表与映射可以循环引用其自身。 每条消息在发送到其他隔离时都会产生副本,以确保 一个隔离无法更改其他隔离中消息的状态。

根据实现的不同,隔离可能分开在不同进程或线程中执行。 对于 web 应用,如果可用,隔离可以编译为 Web worker。

使用隔离

要使用隔离,您需要导入 isolate 库, 创造新隔离,并发送与接收消息。

导入 isolate

隔离相关的 API 可以在 dart:isolate 库中找到。

#import('dart:isolate');

创造新隔离

任意顶级函数或静态方法 都是有效的隔离切入点。 切入点不应要求参数输入,且应返回 void。 使用函数末尾作为隔离切入点是不合法的。 将切入点传递给 spawnFunction()

注:dart2js 尚不支持将静态方法作为隔离切入点。

#import('dart:isolate');

runInIsolate() {
  print("来自隔离中的问候!");
}

main() {
  spawnFunction(runInIsolate);
}

我们计划支持从 URI 所指向的代码创造隔离。

发送消息

可通过 SendPort 将消息发送到隔离。 spawnFunction() 方法会返回指向其 SendPort 的句柄。

如果只是要发送一条消息,可以使用 send()

#import('dart:isolate');

echo() {
  // 在此接收消息,参见下一章节
}

main() {
  SendPort sendPort = spawnFunction(echo);
  sendPort.send("来自 main 的问候");
}

发送任意类型的对象

特殊情况下(如在 Dart VM 内部使用 spawnFunction()) 可以将任意类型的对象实例发送给隔离。 对象消息在发送时会产生副本。

编译到 JavaScript 时, 尚未实现将任意对象实例发送给隔离, 即使使用了 spawnFunction()

接收消息

使用 ReceivePort 可接收发往隔离的消息。可以从 port 属性获取指向 ReceivePort 的句柄。 然后使用传递给 receive() 方法的回调函数处理新消息。

#import('dart:isolate');

echo() {
  port.receive((msg, SendPort reply) {
    print("我收到了:$msg");
  });
}

main() {
  SendPort sendPort = spawnFunction(echo);
  sendPort.send("来自 main 的问候");
}

接收回复

使用 SendPort 的 call() 方法可方便地发送消息与接收回复。 call() 方法会返回 Future 作为回复。

#import('dart:isolate');

echo() {
  port.receive((msg, SendPort reply) {
    reply.send("我收到了:$msg");
  });
}

main() {
  SendPort sendPort = spawnFunction(echo);
  sendPort.call("来自 main 的问候").then((reply) {
    print(reply);    // 我收到了:来自 main 的问候
  });
}

类型定义

在 Dart 中,函数与字符串和数字一样,也是对象。 typedef,或称 函数类型别名,可以对函数命名, 以便在声明语句与返回类型时使用。 当函数类型被赋值给变量时,typedef 会保留类型信息。

请看下述代码,其中未使用 typedef

class SortedCollection {
  Function compare;

  SortedCollection(int f(Object a, Object b)) {
    compare = f;
  }
}

int sort(Object a, Object b) => ... ;

main() {
  SortedCollection collection = new SortedCollection(sort);

  // 我们只知道 compare 是一个函数,但它是哪种类型的函数?
  print(collection.compare is Function);  // true
}

在将 f 赋值给 compare 时,其类型信息丢失。 f 的类型为 (Object, Object) → int,而 compare 的类型为 Function。 如果修改代码,使用确切的名称并保留类型信息, 开发人员与工具便都能利用该信息。

加上 typedef 可让 Dart 保留类型信息。

typedef int Compare(Object a, Object b);

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

int sort(Object a, Object b) => ... ;

main() {
  SortedCollection collection = new SortedCollection(sort);
  print(collection.compare is Function);  // true
  print(collection.compare is Compare);   // true
}

注: 目前,typedef 仅限对函数类型使用。 未来版本的语言规范中可能会有变化。

由于 typedef 只是别名,它们也提供了检查任意函数类型的途径。 例如:

typedef int Compare(int a, int b);

int sort(int a, int b) => a - b;

main() {
  print(sort is Compare);  // true!
}

typedef 还可以参数化。

typedef int Compare<T>(T a, T b);

class SortedCollection<T> {
  Compare<T> compare;
  SortedCollection(this.compare);
}

main() {
  SortedCollection<int> s = new SortedCollection<int>((a,b) => a - b);
  print(s.compare is Compare<int>);  // true
  print(s.compare('a','b'));  // 强制模式下会抛出异常
}

注释

Dart 支持单行注释、多行注释以及文档注释。

单行注释

单行注释以 // 开头。 // 与行尾之间的所有内容都会被 Dart 编译器忽略。

main() {
  // TODO: 以抽象的骆马欢迎方式重构?
  print("欢迎造访我的骆马农场!");
}

多行注释

多行注释以 /* 开头,以 */ 结尾。 /**/ 直接的所有内容都会被 Dart 编译器忽略(除非该注释为文档注释,参见下文)。 多行注释可以嵌套。

main() {
  /*
   * 任务很多。还在考虑养鸡。

  Llama larry = new Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}

文档注释

文档注释是以 /** 开头的多行注释。 Dart 编译器将忽略文档注释中除了括号内部分以外的所有文字。 您可以使用方括号在注释中嵌入类、方法以及字段的链接。 方括号中的名称将被解析为所引用程序元素的词法作用范围。

下面是引用其他类与参数的文档注释示例:

/**
 * 骆马 (Lama glama) 是经驯化的南美骆驼,
 * 自前西班牙时期起被安第斯山脉土著广泛用于
 * 肉类生产与驮运。
 */
class Llama {
  String name;

  /**
   * 给骆马喂 [Food]。通常每周一捆干草。
   */
  feed(Food food) {
    ...
  }

  /**
   * 让骆马做 [timeLimit] 分钟的 [activity] 锻炼。
   */
  exercise(Activity activity, int timeLimit) {
    ...
  }
}

您可以使用 SDK 中附带的 dartdoc 工具解析 Dart 代码并生成 HTML。下面是 dartdoc 输出内容的例子。

<div class="doc">
<p>给骆马喂 <a class="crossref" href="../llama/Food.html">Food</a>。通常每周一捆干草。</p>
<pre class="source">
feed(Food food) {
  // ...
}
</pre>
</div>

请注意 [Food] 是如何从文档注释 转换为指向 Food 类的 API 文档的 HTML 链接的。