An excerpt from Dart: Up and Running

Chapter 3. Dart 库概览

本章介绍如何使用 Dart 库中的主要功能。 这里只是一个概要介绍,并不全面。当你想查看一个类的 详细信息的时候,请参考 Dart API 文档。

dart:core - 数字、集合、 String、以及更多

Dart 核心库提供了少量关键的功能。 该库自动导入到每个 Dart 程序中。

Numbers

dart:core 库定义了 num、 int、 和 double 类, 这些类提供了一些基本的数字操作功能。

通过 int 或者 double 的 parse() 函数可以把字符串转化为 int 或者 double:

lang-dart
ch03/number-tests.dart
assert(int.parse('42') == 42);
assert(int.parse('0x42') == 66);
assert(double.parse('0.50') == 0.5);

或者用 num 的 parse() 函数, 该函数在可能的情况下 会创建一个 整数,如果不是整数,则创建一个 double:

lang-dart
ch03/number-tests.dart
assert(num.parse('42') is int);
assert(num.parse('0x42') is int);
assert(num.parse('0.50') is double);

要指定是几进制数,用可选的 radix 参数:

lang-dart
ch03/number-tests.dart
assert(int.parse('42', radix: 16) == 66);

toString() 函数 (Object 类定义 的函数) 可以把 int 或者 double 转化为 string。 通过 toStringAsFixed() (num 类定义的函数) 可以指定保留几位小数。 通过 toStringAsPrecision() (也是 num 类定义) 可以指定几位有效数字:

lang-dart
ch03/number-tests.dart
// Convert an int to a string.
assert(42.toString() == '42');

// Convert a double to a string.
assert(123.456.toString() == '123.456');

// Specify the number of digits after the decimal.
assert(123.456.toStringAsFixed(2) == '123.46');

// Specify the number of significant figures.
assert(123.456.toStringAsPrecision(2) == '1.2e+2');
assert(double.parse('1.2e+2') == 120.0);

详细信息参考 int, double,num 的 API 文档。 也可以参考下 the section called “dart:math - Math 和 Random”

字符串(String)和正则表达式

Dart 中的字符串是一个不可变的 UTF-16 编码单元(code units) 序列。 在语言概览中详细介绍了 strings。还可以用正则表达式 (RegExp 对象) 来在字符串内搜索和替换部分 字符串 。

String 类有一些函数: split()contains()startsWith()endsWith()、 等等。

在字符串内搜索

你可以查找字符串内匹配字符的开始位置,也可以 检查字符串是否以某个特定的模式开始和结束。 例如:

lang-dart
ch03/string-tests.dart
// 检测一个字符串是否包含另外一个字符串
assert('Never odd or even'.contains('odd'));

// 一个字符串是否以另外一个字符串开始?
assert('Never odd or even'.startsWith('Never'));

// 一个字符串是否以另外一个字符串结束?
assert('Never odd or even'.endsWith('even'));

// 查找一个字符串在另外一个字符串中出现的位置。
assert('Never odd or even'.indexOf('odd') == 6);

从字符串中提取数据

可以从字符串中获取每个独立的字符或者 UTF-16 编码单元。

还可以从字符串中截取一个子字符串或者把一个 字符串分割为一个字符串数组:

lang-dart
ch03/string-tests.dart
// 截取一个子字符串
assert('Never odd or even'.substring(6, 9) == 'odd');

// 使用一个模式来分割字符串
var parts = 'structured web apps'.split(' ');
assert(parts.length == 3);
assert(parts[0] == 'structured');

// 通过下标索引获取一个字符(String 对象)
assert('Never odd or even'[0] == 'N');

// 使用 空字符串作为参数调用 split() 函数可以获取字符串中的
// 所有单个字符,结果为一个内容为 String 的 list
for (var char in 'hello'.split('')) {
  print(char);
}

// 获取一个字符串所有字符的 UTF-16 编码单元
// 有些字符可能需要用两个编码单元来表达
var codeUnitList = 'Never odd or even'.codeUnits.toList();
assert(codeUnitList[0] == 78);

大小写转换

字符串大小写转换非常简单:

lang-dart
ch03/string-tests.dart
// 转换为大小
assert('structured web apps'.toUpperCase() == 'STRUCTURED WEB APPS');

// 转换为小写
assert('STRUCTURED WEB APPS'.toLowerCase() == 'structured web apps');

Note

注意:上面的函数并不适用于所有的语言。 例如 ,土耳其语言中的 I 用上面的函数转换会得到错误的结果。

删除字符串首尾空白字符和检测空字符串

trim() 函数删除字符串首尾的空白字符。 用 isEmpty 来检测字符串是否为空(长度为 0 )。

lang-dart
ch03/string-tests.dart
// 删除字符串首尾空白字符
assert('  hello  '.trim() == 'hello');

// 检测字符串是否为空
assert(''.isEmpty);

// 只有空白字符的字符串并不为空
assert(!'  '.isEmpty);

替代部分字符

String 是不可变对象,所以你只能创建 String 对象而无法修改他们。 如果你仔细的查看 String API 文档,你会发现所有的函数都没有修改 String 的状态。例如, 函数 replaceAll() 返回一个 新的 String 对象而不是修改原来的对象:

lang-dart
ch03/string-tests.dart
var greetingTemplate = 'Hello, NAME!';
var greeting = greetingTemplate.replaceAll(new RegExp('NAME'), 'Bob');

assert(greeting != greetingTemplate); // greetingTemplate didn't change.

创建一个 string

要在代码中生成一个 string, 你可以用 StringBuffer。 只有调用 toString() 函数的时候 StringBuffer 才生成 String 。 writeAll() 函数还有一个可选的参数可以设置 字符之间的分隔符,在下面的示例中用的分隔符的空格。

lang-dart
ch03/string-tests.dart
var sb = new StringBuffer();

sb..write('Use a StringBuffer ')
  ..writeAll(['for', 'efficient', 'string', 'creation'], ' ')
  ..write('.');

var fullString = sb.toString();

assert(fullString ==
    'Use a StringBuffer for efficient string creation.');

正则表达式

RegExp 类提供了 JavaScript 正则表达式的功能。 用正则表达式搜索字符串和匹配符合某个规则的字符串更加高效。

lang-dart
ch03/string-tests.dart
// 匹配一个或者多个数字的规则
var numbers = new RegExp(r'\d+');

var allCharacters = 'llamas live fifteen to twenty years';
var someDigits = 'llamas live 15 to 20 years';

// contains() 可以用正则表达式作为参数
assert(!allCharacters.contains(numbers));
assert(someDigits.contains(numbers));

// 用一个字符串替换所有匹配的字符
var exedOut = someDigits.replaceAll(numbers, 'XX');
assert(exedOut == 'llamas live XX to XX years');

你也可以直接操作 RegExp 对象。 Match 类提供了访问匹配一个表达式的结果:

lang-dart
ch03/string-tests.dart
var numbers = new RegExp(r'\d+');
var someDigits = 'llamas live 15 to 20 years';

// 检测字符串中是否有匹配的项
assert(numbers.hasMatch(someDigits));

// 遍历所有匹配的项
for (var match in numbers.allMatches(someDigits)) {
  print(match.group(0)); // 15, then 20
}

更多信息

参考 String API 文档 来查看 String 的所有函数。同时参考 StringBuffer, Pattern, RegExp, and Match 类的 API 文档来了解这些类的更多功能。

集合

Dart 包含了一个核心的集合 API,这些 API 包含 lists、 sets、和 maps。

Lists

在语言概览中已经介绍过用 字符字面量来创建 lists 的方法。 另外也可以用 List 的构造函数。 List 还定义了一些函数可以添加或者删除 List 中的内容。

lang-dart
ch03/list-tests.dart
// 使用 List 构造函数
var vegetables = new List();

// 使用 list 字符字面量 创建 List
var fruits = ['apples', 'oranges'];

// 添加到 list 中
fruits.add('kiwis');

// 一次添加多个项到 list 中
fruits.addAll(['grapes', 'bananas']);

// 查询 list 的长度
assert(fruits.length == 5);

// 删除一个项
var appleIndex = fruits.indexOf('apples');
fruits.removeAt(appleIndex);
assert(fruits.length == 4);

// 删除 list 中的所有项
fruits.clear();
assert(fruits.length == 0);

indexOf() 函数来查找 list 中 某个位置的对象:

lang-dart
ch03/list-tests.dart
var fruits = ['apples', 'oranges'];

// 通过索引访问 list 中的对象
assert(fruits[0] == 'apples');

// 查找 list 中是否包含该对象
assert(fruits.indexOf('apples') == 0);

sort() 函数可以排序 list 中的内容。 该函数的参数为一个用来比较两个对象的方法。该方法的返回值需要满足如下要求: 比较的结果为 小于的话,则返回值必需 < 0, 如果相等的话,则必需返回 0,如果结果为 大于的话,则返回值必需为 > 0 。 下面的示例使用了 Comparable 接口定义的函数 compareTo(), String 类实现了该函数。

lang-dart
ch03/list-tests.dart
var fruits = ['bananas', 'apples', 'oranges'];

// 排序 list 中的对象
fruits.sort((a, b) => a.compareTo(b));
assert(fruits[0] == 'apples');

List 支持泛型,所以你可以设置一个 List 中对象的类型:

lang-dart
ch03/list-tests.dart
// 这个 list 应该只包含 String 对象
var fruits = new List<String>();

fruits.add('apples');
var fruit = fruits[0];
assert(fruit is String);

// 将有一个静态分析警告, num 不是一个 String
fruits.add(5);  // BAD: 在检测模式(Checked mode)下会抛出异常。

参考 List API 文档 来了解 List 对象的所有函数,

Sets

Dart 中的 Set 是包含唯一对象的无序集合。 由于 set 中的对象都是没有顺序的,所以无法用位置索引来 访问里面的对象。

lang-dart
ch03/set-tests.dart
var ingredients = new Set();
ingredients.addAll(['gold', 'titanium', 'xenon']);
assert(ingredients.length == 3);

// 添加一个重复的对象不起作用
ingredients.add('gold');
assert(ingredients.length == 3);

// 从 set 中删除一个对象
ingredients.remove('gold');
assert(ingredients.length == 2);

contains()containsAll() 来检测 set 中是否包含一个或者多个对象:

lang-dart
ch03/set-tests.dart
var ingredients = new Set();
ingredients.addAll(['gold', 'titanium', 'xenon']);

// 检测一个对象是否在 set 中
assert(ingredients.contains('titanium'));

// 检测是否 set 包含多个对象
assert(ingredients.containsAll(['titanium', 'xenon']));

交集是两个 set 中都包含的对象集合。

lang-dart
ch03/set-tests.dart
var ingredients = new Set();
ingredients.addAll(['gold', 'titanium', 'xenon']);

// 创建两个 set 的交集
var nobleGases = new Set.from(['xenon', 'argon']);
var intersection = ingredients.intersection(nobleGases);
assert(intersection.length == 1);
assert(intersection.contains('xenon'));

参考 Set API 文档 了解 set 的更多功能。

Maps

map 通常被称之为 dictionary 或者 hash, map 是一个无序的 key-value(键-值) 对。 map 中每个值都有一个对应的键便于访问。和 JavaScript 不同, Dart 对象不是 map。

可以通过字符字面量定义 map 也可以通过 map 构造函数来定义 map:

lang-dart
ch03/map-1.dart
// map 通常都是用 String 做为 key
var hawaiianBeaches = {
  'oahu'       : ['waikiki', 'kailua', 'waimanalo'],
  'big island' : ['wailea bay', 'pololu beach'],
  'kauai'      : ['hanalei', 'poipu']
};

// 用构造函数创建 map
var searchTerms = new Map();

// map 支持泛型,所以可以指定 map 的 key 和 value 的类型:
var nobleGases = new Map<int, String>();

使用方括号来添加、设置和访问 map 中的值。用 remove() 函数从 map 中删除 对应的 key 和 value。 [注意 下面的示例中使用了 尚未见到过的 containsKey 函数]

lang-dart
ch03/map-1.dart
var nobleGases = { 54: 'xenon' };

// 通过 key 获取对应的 value
assert(nobleGases[54] == 'xenon');

// 检测 map 中是否包含一个 key
assert(nobleGases.containsKey(54));

// 删除一个 key 和对应的 value
nobleGases.remove(54);
assert(!nobleGases.containsKey(54));

可以获取 map 的所有 key 或者所有 value:

lang-dart
ch03/map-1.dart
var hawaiianBeaches = {
  'oahu' : ['waikiki', 'kailua', 'waimanalo'],
  'big island' : ['wailea bay', 'pololu beach'],
  'kauai' : ['hanalei', 'poipu']
};

// 获取所有的 key,结果为一个无序集合 ( 一个 Iterable).
var keys = hawaiianBeaches.keys;

assert(keys.length == 3);
assert(new Set.from(keys).contains('oahu'));

// 获取 map 所有 value 的集合 (an Iterable of Lists).
var values = hawaiianBeaches.values;
assert(values.length == 3);
assert(values.any((v) => v.contains('waikiki')));

containsKey() 函数检测 map 中是否包含一个 key。由于 map 的 value 可以为 null, 所以你不能通过判断 key 来判断是否包含一个 value。

lang-dart
ch03/map-1.dart
var hawaiianBeaches = {
  'oahu' : ['waikiki', 'kailua', 'waimanalo'],
  'big island' : ['wailea bay', 'pololu beach'],
  'kauai' : ['hanalei', 'poipu']
};

assert(hawaiianBeaches.containsKey('oahu'));
assert(!hawaiianBeaches.containsKey('florida'));

如果你想在 key 不存在的时候 才设置该值,则可以用 putIfAbsent() 函数。该函数 的参数为返回设置的值的方法:

lang-dart
ch03/map-1.dart
var teamAssignments = {};
teamAssignments.putIfAbsent('Catcher', () => pickToughestKid());
assert(teamAssignments['Catcher'] != null);

参考 Map API 文档 查看 Map 的所有函数。

常用的集合方法

List、 Set、 和 Map 公用一些常用的 集合操作。有些函数定义在 Iterable 类中, List 和 Set 实现了该类。

Note

虽然 Map 没有实现 Iterable,但是通过 Map 的 keysvalues 属性可以得到一个 Iterable 对象。

isEmpty 函数来检查 集合是否为空:

lang-dart
ch03/collection-isEmpty.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];
assert(!teas.isEmpty);

要在 list、 set、 或者 map 中的每个元素上使用一个方法,则可以用 forEach() 函数:

lang-dart
ch03/collection-apply-function.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];

teas.forEach((tea) => print('I drink $tea'));

当调用 Map 的 forEach() 函数的时候, 方法参数必须有两个参数(分别为 key 和 value):

lang-dart
ch03/map-1.dart
hawaiianBeaches.forEach((k, v) {
  print('I want to visit $k and swim at $v');
  // I want to visit oahu and swim at [waikiki, kailua, waimanalo], etc.
});

还有一个用来遍历集合的方法:map() ,该函数把所有的结果放到一个对象中:

lang-dart
ch03/collection-apply-function.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];

var loudTeas = teas.map((tea) => tea.toUpperCase());
loudTeas.forEach(print);

Note

注意: map() 函数返回的 Iterable 对象是 lazily evaluated(懒惰加载的): 只有当你使用返回对象的时候,你的方法才被调用。

要强制立刻在每个对象上调用你的方法,则可以用 map().toList() 或者 map().toSet() 函数:

lang-dart
ch03/collection-apply-function.dart
var loudTeaList = teas.map((tea) => tea.toUpperCase()).toList();

Iterable 的 where() 函数用户过滤集合中的对象, 返回符合条件的所有对象。 Iterable 的 any()every() 函数用来检查是否一些对象或者所有对象都满足 一个条件。

lang-dart
ch03/collection-any-every.dart
var teas = ['green', 'black', 'chamomile', 'earl grey'];

// Chamomile is not caffeinated.
bool isDecaffeinated(String teaName) => teaName == 'chamomile';

// Use where() to find only the items that return true
// from the provided function.
var decaffeinatedTeas = teas.where((tea) => isDecaffeinated(tea));
// or teas.where(isDecaffeinated)

// Use any() to check whether at least one item in the collection
// satisfies a condition.
assert(teas.any(isDecaffeinated));

// Use every() to check whether all the items in a collection
// satisfy a condition.
assert(!teas.every(isDecaffeinated));

参考 Iterable API 文档 和 List、 Set、 以及 Map 的文档来了解集合的所有函数。

URIs

Uri 类 提供了用来编码和解码 URI 字符串的函数。 这些函数处理 URI 中的特殊字符,例如 &=。 Uri 类还提供了获取 URI 组件的函数, 例如 URI 的 主机地址、端口号、协议 等等。

编码和解码完整的 URI

使用 encodeFull()decodeFull() 函数来编码和解码 除了 特殊字符的 URI(例如这些特殊字符:/, :, &, #)。 这些函数特别擅长 用来编码和解码一个完整的 URI,保留 特殊的 URI 字符。

lang-dart
code/ch03/encodeUri.dart
main() {
  var uri = 'http://example.org/api?foo=some message';
  var encoded = Uri.encodeFull(uri);
  assert(encoded == 'http://example.org/api?foo=some%20message');

  var decoded = Uri.decodeFull(encoded);
  assert(uri == decoded);
}

注意,上面的代码中,只有位于 somemessage 之间的空格给编码了。

编码和解码 URI 组件

使用encodeComponent()decodeComponent() 函数来编码和解码 URI 组件, URI 组件中的特殊字符(例如 /, &, and :)会被 正确的编码。

lang-dart
code/ch03/encodeUriComponents.dart
main() {
  var uri = 'http://example.org/api?foo=some message';
  var encoded = Uri.encodeComponent(uri);
  assert(encoded == 'http%3A%2F%2Fexample.org%2Fapi%3Ffoo%3Dsome%20message');

  var decoded = Uri.decodeComponent(encoded);
  assert(uri == decoded);
}

注意上面特殊的字符是如何编码的。例如 / 被编码为 %2F

解析 URI

如果你有一个 Uri 对象或者一个 URI 字符串,使用 Uri 的变量可以 获取 Uri 的组成部分,例如 path(路径)。 使用 parse() 静态函数把 URI 字符串解析为 Uri 对象。

lang-dart
code/ch03/parseUri.dart
main() {
  var uri = Uri.parse('http://example.org:8080/foo/bar#frag');

  assert(uri.scheme   == 'http');
  assert(uri.host     == 'example.org');
  assert(uri.path     == '/foo/bar');
  assert(uri.fragment == 'frag');
  assert(uri.origin   == 'http://example.org:8080');
}

参考 Uri API 文档 了解 Uri 的所有函数。

创建 URI

可以用 Uri() 构造函数来创建一个 URI:

lang-dart
code/ch03/uriFromComponents.dart
main() {
  var uri = new Uri(scheme: 'http', host: 'example.org', 
                    path: '/foo/bar', fragment: 'frag');
  assert(uri.toString() == 'http://example.org/foo/bar#frag');
}

日期和时间

DateTime 对象代表某个时间。时区不是 UTC 就是 本地时区。

可以用如下构造函数创建 DateTime 对象:

lang-dart
ch03/date.dart
// 获取当前的日期和时间
var now = new DateTime.now();

// 使用本地时区创建一个 DateTime 对象
var y2k = new DateTime(2000);   // January 1, 2000

// 设置年月日
y2k = new DateTime(2000, 1, 2); // January 2, 2000

// 创建一个 UTC 时间
y2k = new DateTime.utc(2000);   // January 1, 2000, UTC

// 用从 Unix epoch 开始的毫秒数来设置时间
y2k = new DateTime.fromMillisecondsSinceEpoch(946684800000, isUtc: true);

// 解析 ISO 8601 格式日期
y2k = DateTime.parse('2000-01-01T00:00:00Z');

millisecondsSinceEpoch 的值为 从 “Unix epoch”—1970年1月1号 UTC 时区 开始的毫秒数值:

lang-dart
ch03/date.dart
var y2k = new DateTime.utc(2000);           // 1/1/2000, UTC
assert(y2k.millisecondsSinceEpoch == 946684800000);
var unixEpoch = new DateTime.utc(1970); // 1/1/1970, UTC
assert(unixEpoch.millisecondsSinceEpoch == 0);

用 Duration 类来计算两个日期之间的间隔,或者在当前时间上加减时间:

lang-dart
ch03/date.dart
var y2k = new DateTime.utc(2000);

// 在当前时间上加一年
var y2001 = y2k.add(const Duration(days: 366));
assert(y2001.year == 2001);

// 减去30天
var december2000 = y2001.subtract(const Duration(days: 30));
assert(december2000.year == 2000);
assert(december2000.month == 12);

// 计算两个日期之间的间隔
// 返回一个 Duration 对象
var duration = y2001.difference(y2k);
assert(duration.inDays == 366); // y2k 是闰年

Warning

Using a Duration to shift a DateTime by days can be problematic, due to clock shifts (to daylight saving time, for example). Use UTC dates if you must shift days.

参考 DateTimeDuration 的 API 文档来了解这些类的所有函数。

常用工具类

核心库还包含用来拍下、map 的 key 和遍历对象的各种工具类。

比较对象

实现 Comparable 接口的对象可以和另外一个对象比较,通常用来排序。 compareTo() 函数 的返回结果如下:小于 的返回值为 < 0;相等 的返回值为 0;大于 的返回值为 > 0。

lang-dart
ch03/comparable.dart
class Line implements Comparable {
  final length;
  const Line(this.length);
  int compareTo(Line other) => length - other.length;
}

main() {
  var short = const Line(1);
  var long = const Line(100);
  assert(short.compareTo(long) < 0);
}

实现 map 的 key

Dart 中的每个对象都自动带有一个整数的 哈希值,所有可以用作 map 中的 key。 你还可以重写(override) hashCode getter 函数来生成一个自定义 的哈希值。如果你自定义了哈希值,记得同时还要重写 == 操作符。相等的对象 (用 == 来比较) 必须具有同样的哈希值。 一个哈希值不一定是唯一的,但是应该具有合理的分布。

注意: 在 == 实现中包含 identical() 是有争议的。虽然这样可以 提高速度。但是通常 NaN != NaN ,所以 对象默认并不是 identical() 的。

lang-dart
ch03/map-keys.dart
class Person {
  final String firstName, lastName;

  Person(this.firstName, this.lastName);

  // 使用 Effective Java 第 11 章 中的方法来重写 hashCode 函数
  int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
  }

  // 如果重写了 hashCode 则需要同时重写 operator== 
  bool operator==(other) {
    if (other is! Person) return false;
    Person person = other;
    return (person.firstName == firstName && person.lastName == lastName);
  }
}

main() {
  var p1 = new Person('bob', 'smith');
  var p2 = new Person('bob', 'smith');
  var p3 = 'not a person';
  assert(p1.hashCode == p2.hashCode);
  assert(p1 == p2);
  assert(p1 != p3);
}

Iteration(遍历)

IterableIterator 类支持 for-in 循环。 如果你创建的对象需要支持 for-in 循环,则可以继承(Extends)或者实现(Implements) Iterable 接口。 实现 Iterator 来定义遍历的功能。

lang-dart
ch03/iterator.dart
class Process {
  // Represents a process...
}

class ProcessIterator implements Iterator<Process> {
  Process current;
  bool moveNext() {
    return false;
  }
}

// 一个可以遍历所有 Process 的虚构类
// 继承 Iterable 的一个子类。
class Processes extends IterableBase<Process> {
  final Iterator<Process> iterator = new ProcessIterator();
}

main() {
  // Iterable 对象可以在 for-in 循环中使用
  for (var process in new Processes()) {
    // Do something with the process.
  }
}

异常(Exceptions)

Dart 核心库定义了很多常用的异常(exception)和错误(error)。 异常是一些你可以提前预料和处理的错误情况。而错误则是你没有料到或者没有处理的情况。

一些常见的错误如下:

NoSuchMethodError

当一个对象(可能为 null) 没有实现一个 被调用的函数 时抛出该错误。

ArgumentError

当用错误的参数调用一个函数的时候,由该函数抛出。

抛出一个应用相关的异常是表明发生错误的常见做法。 你可以通过实现 Exception 接口 来定义自己的异常:

lang-dart
ch03/exceptions.dart
class FooException implements Exception {
  final String msg;
  const FooException([this.msg]);
  String toString() => msg == null ? 'FooException' : msg;
}

更多信息请参考 the section called “Exceptions(异常)”Exception API 文档.

dart:async - 异步编程

异步编程通常使用回调函数,而 Dart 提供了另外一种选择:FutureStream 对象。一个 Future 对象保证在未来的某个时间返回一个结果。 Stream 对象是获取一系列值的方法,例如 输入事件序列。 dart:async 库提供了 Future、 Stream、和其他更多异步编程支持。

在命令行应用和 web 应用中都可以使用 dart:async 库。 要使用该库,只需要导入 dart:async:

lang-dart
import 'dart:async';

Future

在 Dart 库中到处都可以看到 Future 对象, 通常都是作为异步函数的返回值出现。 当一个 future 完成的时候,返回的值就可以使用了。

基础用法

使用 then() 来设置当 future 完成时执行的代码。 例如,由于 HTTP 请求可以消耗一些时间,所以 HttpRequest.getString() 返回一个 Future。 使用 then() 你可以在 Future 完成的时候来执行一些代码:

lang-dart
ch03/ch03_4_async/web/ch03_4_async.dart
HttpRequest.getString(url)
  .then((String result) {
    print(result); });
  // 在这里处理错误情况

catchError() 来处理 Future 对象 抛出的异常。

lang-dart
ch03/ch03_4_async/web/ch03_4_async.dart
HttpRequest.getString(url)
  .then((String result) {  // 回调函数
    print(result); })
  .catchError((e) {
    // 处理或者忽略错误情况
  });

这里的 then().catchError() 模式是 异步版本的 try-catch 模式。

Note

注意:要在 then() 上 调用 catchError() ,而不是在原来的 Future 上调用。否则的话, catchError() 只能处理 Future 中的 异常而无法 处理 then() 中的异常。

串联多个异步函数

由于 then() 函数返回一个 Future , 所以提供了一种按照顺序执行多个异步方法的方式。 如果 then() 的回调函数返回一个 Future, 则 then() 直接返回该对象。 如果回调函数返回其他类型的值,则 返回一个新的 Future 对象,该 Future 对象完成时候 返回回调函数返回的值。

lang-dart
ch03/futures.dart
Future result = costlyQuery();

return result.then((value) => expensiveWork())
             .then((value) => lengthyComputation())
             .then((value) => print('done!'))
             .catchError((exception) => print('DOH!'));

上面的示例代码中,按照如下顺序执行:

  1. costlyQuery()

  2. expensiveWork()

  3. lengthyComputation()

等待多个 Futures 完成

有时,你的算法要求调用多个异步 方法并且等待所有的方法都完成。使用 Future.wait() 静态函数来管理多个 Future 并等待所有 Future 一起完成:

lang-dart
ch03/futures.dart
Future deleteDone = deleteLotsOfFiles();
Future copyDone = copyLotsOfFiles();
Future checksumDone = checksumLotsOfOtherFiles();

Future.wait([deleteDone, copyDone, checksumDone]).then((List values) {
  print('Done with all the long steps');
});

Stream

Stream 对象代表数据序列,也经常在 Dart API 中出现。 例如, HTML 的按钮点击事件就是用 stream 来传输的。 你也可以用 stream 来读取文件。

监听 Stream 数据流

要获取每个 Stream 中的数据,则可以用 listen() 函数:

lang-dart
ch03_html/ch03_html.dart
// 通过 ID 查找按钮并注册事件
querySelector('#submitInfo').onClick.listen((e) {
  // 当按钮点击的时候,执行下面代码
  submitData();
});

上面的例子中,'submitInfo' 按钮的 onClick 属性为一个 Stream 对象。

如果你只关心第一个事件,则可以用 firstlast 或者 single 属性。 用 firstWhere()lastWhere()、或者 singleWhere() 函数在处理数据之前测试事件发生的位置。 {待定:示例。}

如果你关心一段数据,则可以用 skip()skipWhile()take()takeWhile()、 和 where() 函数。 {PENDING: example}

转换 Stream 数据

通常,在使用 Stream 数据之前都需要先转换为可用形式。 用 transform() 函数来把 Stream 数据转换为其他 形式:

lang-dart
ch03/readFile.dart
var config = new File('config.txt');
Stream<List<int>> inputStream = config.openRead();

inputStream
  .transform(UTF8.decoder)
  .transform(new LineSplitter())
  .listen(
    (String line) {...} ...);

这里用了两个转换器。 第一个转换器 UTF8.decoder 把 Stream<List<int>> 转换为 一个 Stream<String>。然后用 一个 LineSplitter 把 Stream<String> 转换为 Stream<List<String>> 对象。 这些转换器来自于 dart:convert 库。 (参考 the section called “dart:convert - 编码和解码 JSON 数据、 UTF-8 编码以及其他”)。 PENDING: add onDone and onError. (See "Streaming file contents".)

更多信息

在命令行应用中使用 Future 和 Stream 的例子请参考 the section called “dart:io - 用于命令行应用的 I/O 操作 ”。 下面还有一些可以参考的文章:

dart:math - Math 和 Random

Math 库提供了数学运算的常用公式,例如 sine 和 cosine、 最大值和最小值、另外还有一些常量 pie 等。 Math 库中的大部分方法都是顶级方法。

在代码中 import dart:math 即可使用 Math 库。 下面的示例中,使用前缀 math 来表明 使用了 Math 库中的顶级函数和常量,这样使代码看起来 更加清晰:

lang-dart
ch03/math-tests.dart
import 'dart:math' as math;

三角函数

Math 库提供了常用的三角函数:

lang-dart
ch03/math-tests.dart
// Cosine
assert(math.cos(math.PI) == -1.0);

// Sine
var degrees = 30;
var radians = degrees * (math.PI / 180);
// radians is now 0.52359.
var sinOf30degrees = math.sin(radians);

// Truncate the decimal places to 2.
assert(double.parse(sinOf30degrees.toStringAsPrecision(2)) == 0.5);

Note

这些函数的参数为弧度不是角度!

最大值和最小值

Math 库还提供了优化过的 max()min() 函数:

lang-dart
ch03/math-tests.dart
assert(math.max(1, 1000) == 1000);
assert(math.min(1, -1000) == -1000);

数学常量

Math 库提供了很多常量,例如 pie 等:

lang-dart
ch03/math-tests.dart
// See the Math library for additional constants.
print(math.E);     // 2.718281828459045
print(math.PI);    // 3.141592653589793
print(math.SQRT2); // 1.4142135623730951

随机数

Random 类来生成随机数。 还可以用一个种子来构造一个 Random 对象。

lang-dart
ch03/math-tests.dart
var random = new math.Random();
random.nextDouble(); // 返回值在 0.0 和 1.0 之间: [0, 1)
random.nextInt(10);  // 返回值在 0 和 9 之间

甚至还可以生成一个随机布尔值:

lang-dart
ch03/math-tests.dart
var random = new math.Random();
random.nextBool();  // true or false

更多信息

参考 Math API 文档 以及 num、 int、double 来 查看所有的函数。

dart:html - 基于浏览器的应用

使用 dart:html 库 来编写浏览器应用,该库提供了操作 DOM 元素和访问 HTML5 API 的功能。 DOMDocument Object Model 的缩写,DOM 描述了 HTML 页面的文档结构。

使用 dart:html 库还可以用来操作 (CSS)、用 HTTP 请求获取数据、以及 用 WebSockets 来传输数据。 HTML5 (和 dart:html) 还有很多本节没有介绍的 API。 注意: 只有 Web 应用可以使用 dart:html 库,命令行应用无法使用。

Note

使用 Polymer.dartAngularDart 可以创建更灵活、更具扩展性的 web 应用 UI。

导入 dart:html 库即可使用该库:

lang-dart
ch03_html/ch03_html.dart
import 'dart:html';

Note

有些 dart:html 库的功能还是试验性质的,在 API 文档中有注明。

操作 DOM

要使用 DOM 你需要了解 windows(窗口)documents(文档)elements(元素)、 和 nodes(节点)

一个 Window 对象代表 浏览器的真实窗口。 每个 Window 都有一个 document 属性( 一个 Document 对象),该对象代表当前加载的页面内容。 Window 对象还包含了访问各种 API 的能力,例如 IndexedDB (用来 存储数据)、 requestAnimationFrame() (用来实现动画)、以及更多。 在支持标签页(tab)的浏览器中,每个标签页都有自己的 Window 对象。

使用 Document 对象可以操作文档中的 Elements 。 注意 Document 本身也是一个 element,也可以被修改。

DOM 是一个 Nodes 树模型。 这些 Node 通常都是 Element,有些是 attributes(属性)、 text(文本)、 comments(注释)、 以及其他类型。除了根 Node 没有父节点外,DOM 中的所有 Node 都有一个父节点,每个节点都可能有多个子节点。

查找 elements

你需要先找到一个 element 才能修改该 element。 你可以用各种条件查找一个 element 对象。

使用顶级方法 querySelector() querySelectorAll() 来查找一个或者多个 element。 可以用 ID、类(class)、tag(标签)、name(名字)、或者其组合来查找 Element。 CSS Selector Specification guide 定义了选择器的规范。 例如,用 # 前缀指定 ID;用 . 指定 CSS 类。

querySelector() 方法返回第一个匹配 的 Element,而 querySelectorAll() 返回匹配的所有 Element。

lang-dart
ch03_html/ch03_html.dart
Element elem1 = querySelector('#an-id');           // Find an element by id (an-id).
Element elem2 = querySelector('.a-class');         // Find an element by class (a-class).
List<Element> elems1 = querySelectorAll('div');    // Find all elements by tag (<div>).
List<Element> elems2 = querySelectorAll('input[type="text"]'); // Find all text inputs.

// Find all elements with the CSS class 'class' inside of a <p>
// that is inside an element with the ID 'id'.
List<Element> elems3 = querySelectorAll('#id p.class');

操控 elements

可以用 properties(属性) 来修改 element 的状态。 Node 和子类型 Element 定义了所有 element 都有的 属性。 例如, 所有的 element 都有 classeshiddenidstyle、 和 title 属性,可以用这些属性来 设置 element 的状态。 Element 的子类定义了其他属性,例如 AnchorElement 定义了 href 属性。

下面是一个设置 HTML 超链接的示例:

lang-dart
ch03_html/ch03_html.dart
<a id="example" href="http://example.com">link text</a>

这个 <a> 标签定义了一个带有 href 属性和 文本节点( text node) (通过 text 属性访问该文本) 的 链接。 可以使用 AnchorElement 的 href 属性来 修改该链接打开的 URL 地址:

lang-dart
ch03_html/ch03_html.dart
querySelector('#example').href = 'http://dartlang.org';

通常你需要修改多个 element 的属性。 例如,下面的代码设置使用类 “mac”、 “win”、 或者 “linux” 元素的 hidden 属性。 把 hidden 属性设置为 true 和 在 CSS 样式上 添加 display:none 是一样的效果。

lang-dart
ch03_html/ch03_html.dart
<!-- In HTML: -->
<p>
  <span class="os linux">Words for Linux</span>
  <span class="os mac">Words for Mac</span>
  <span class="os win">Words for Windows</span>
</p>

// In Dart:
final osList = ['mac', 'win', 'linux'];

var userOs = 'linux'; // In real code you'd programmatically determine this.

for (var os in osList) {            // For each possible OS...
  bool shouldShow = (os == userOs); // Does this OS match the user's OS?
  for (var elem in querySelectorAll('.$os')) { // Find all elements for this OS.
    elem.hidden = !shouldShow;      // Show or hide each element.
  }
}

当所需要的属性不可用或者不方便使用的话,可以用 Element 对象的 attributes 属性。 该属性为一个 Map<String, String> 对象, key 为 attribute 的名字。 参考 MDN Attributes 页面 来查看每个属性代表的意义。 下面是一个设置 attribute 值的示例:

lang-dart
ch03_html/ch03_html.dart
elem.attributes['someAttribute'] = 'someValue';

创建 elements

创建一个新的 elements 并添加到 DOM 树中可以往 HTML 页面中添加内容。 下面是一个创建 段落((<p>)) 的示例:

lang-dart
ch03_html/ch03_html.dart
var elem = new ParagraphElement();
elem.text = 'Creating is easy!';

通过解析 HTML 文本也可以创建 Element 对象。 所有的子 element 也会被解析并创建。

lang-dart
ch03_html/ch03_html.dart
var elem2 = new Element.html('<p>Creating <em>is</em> easy!</p>');

注意:上面示例中的 elem2 对象为 ParagraphElement 。

通过设置新创建 element 对象的父节点来把该 element 添加到 文档中。你可以把一个 element 添加到现有 element 的子节点上。 下面的示例中, body 是一个 element, 使用 children property(属性)可以 访问 body 的子元素(as a List<Element>)。

lang-dart
ch03_html/ch03_html.dart
document.body.children.add(elem2);

添加、替换、删除节点( node )

还记得 element 是 node 的子类型吗。使用 Node 的 nodes 属性可以获取该 node 的所有子节点, 返回值为 List<Node>。 获取到该 list 后,就可以用 List 的函数和操作符来操作 node 的子节点了。

使用 List 的 add() 函数把一个节点添加到父节点的所有子节点的最后:

lang-dart
ch03_html/ch03_html.dart
// Find the parent by ID, and add elem as its last child.
querySelector('#inputs').nodes.add(elem);

用 Node 的replaceWith() 函数来替换一个 node:

lang-dart
ch03_html/ch03_html.dart
// Find a node by ID, and replace it in the DOM.
querySelector('#status').replaceWith(elem);

使用 Node 的 remove() 函数来删除节点:

lang-dart
ch03_html/ch03_html.dart
// Find a node by ID, and remove it from the DOM.
querySelector('#expendable').remove();

控制 CSS 样式

CSS (cascading style sheets)定义了 DOM 元素的显示样式。 通过设置 ID 和 类属性可以修改 一个 element 的显示样式。

每个 element 都有一个 classes 变量,该变量是一个 list 对象。 在该 list 中添加、删除 字符串就可以实现 添加和删除 CSS 类的功能。 例如,下面的示例在 element 中添加一个 warning CSS 类:

lang-dart
ch03_html/ch03_html.dart
var element = querySelector('#message');
element.classes.add('warning');

通过 ID 来查找 element 一般都是比较高效的。 通过 id property(属性) 可以 动态的设置一个 element 的 ID。

lang-dart
ch03_html/ch03_html.dart
var message = new DivElement();
message.id = 'message2';
message.text = 'Please subscribe to the Dart mailing list.';

通过级联操作符可以减少输入敲键盘的次数:

lang-dart
ch03_html/ch03_html.dart
var message = new DivElement()
    ..id = 'message2'
    ..text = 'Please subscribe to the Dart mailing list.';

使用 ID 或者 类来设置 element 的样式是最好的做法,但是有时候你也需要 直接在 element 上设置样式:

lang-dart
ch03_html/ch03_html.dart
message.style
    ..fontWeight = 'bold'
    ..fontSize = '3em';

处理事件

要响应外部事件,例如 鼠标点击、焦点改变和选择 等,需要注册一个事件监听器(event listener)。 你可以在页面的任意 element 上注册事件监听器。 如果你初次接触 web 编程,请参考 这里了解事件分发和传播 机制。

使用 element.onEvent.listen(function) 来注册事件监听器,这里的 Event 是事件的名字, function 是事件 处理函数。

例如,下面是一次处理按钮点击事件的示例:

lang-dart
ch03_html/ch03_html.dart
// Find a button by ID and add an event handler.
querySelector('#submitInfo').onClick.listen((e) {
  // When the button is clicked, it runs this code.
  submitData();
});

事件可以在 DOM 树中上下传递。 使用 e.target 来判断是哪个 element 触发了该事件:

lang-dart
ch03_html/ch03_html.dart
document.body.onClick.listen((e) {
  var clickedElem = e.target;
  print('You clicked the ${clickedElem.id} element.');
});

通过查看Element 和其子类的 "onEventType" 属性可以了解都有 那些事件类型。一些常见的示例类型如下:

  • change

  • blur

  • keyDown

  • keyUp

  • mouseDown

  • mouseUp

用 HttpRequest 请求 HTTP 资源

HttpRequest 类(之前被称之为 XMLHttpRequest ) 可以在浏览器中访问 HTTP 资源。 传统的 AJAX 风格应用非常依赖 HttpRequest。 用 HttpRequest 来从服务器动态加载 JSON 数据或者其他资源。 还可以动态的给服务器发送数据。

下面的示例中假设所请求的资源和 脚本本事位于同一个 服务器。 由于浏览器的安全策略限制,HttpRequest 类不能简单的使用不再同一个主机的资源。 如果你需要访问不同主机的资源,你可以用 JSONP 技术或者启用 远程资源的 CORS 头(header)。

从服务器获取数据

HttpRequest 类的静态函数 getString() 是从服务器获取数据的一个方法。在 getString() 的返回值上使用 then() 来异步处理返回的结果。

lang-dart
ch03_html/ch03_2_html.dart
import 'dart:html';
import 'dart:async';

// A JSON-formatted file in the same location as this page.
var uri = 'data.json';

main() {
  // Read a JSON file.
  HttpRequest.getString(uri).then(processString);
}

processString(String jsonText) {
  parseText(jsonText);
}

上面设置的方法 processString() 会在请求完成的时候执行。 上面的示例中,动态的加载一个 JSON 文件。在 the section called “编码和解码 JSON” 介绍了 JSON API。

.then() 后面通过 .catchError() 来设置一个错误处理方法:

lang-dart
ch03_html/ch03_2_html.dart
...
HttpRequest.getString(uri)
    .then(processString)
    .catchError(handleError);
...
handleError(error) {
  print('Uh oh, there was an error.');
  print(error.toString());
}

如果你的 HttpRequest 返回的结果不是一个字符串,则 可以用 request() 静态函数。 下面是读取 XML 数据的一个示例:

lang-dart
ch03_html/ch03_2_html.dart
import 'dart:html';
import 'dart:async';

// An XML-formatted file in the same location as this page.
var xmlUri = 'data.xml';

main() {
  // Read an XML file.
  HttpRequest.request(xmlUri)
      .then(processRequest)
      .catchError(handleError);
}

processRequest(HttpRequest request) {
  var xmlDoc = request.responseXml;
  try {
    var license = xmlDoc.querySelector('license').text;
    print('License: $license');
  } catch(e) {
    print('$xmlUri doesn\'t have correct XML formatting.');
  }
}
...

还可以用完整的 API(full API) 来实现不同的功能, 例如读取请求头信息。

下面是使用 完整 API 的流程:

  1. 创建一个 HttpRequest 对象:

  2. GET 或者 POST 打开一个 URL。

  3. 添加事件处理函数。

  4. 发送该请求。

例如:

lang-dart
dart-tutorials-samples/web/portmanteaux/portmanteaux.dart
import 'dart:html';
...
var httpRequest = new HttpRequest()
    ..open('POST', dataUrl)
    ..onLoadEnd.listen((_) => requestComplete(httpRequest))
    ..send(encodedData);

往服务器发送数据

HttpRequest 用 HTTP POST 可以往服务器发送数据。 例如:你可能需要从 form 表单中动态的提交数据。 往 RESTful web 服务 发送 JSON 数据是另外 一个常见的情景。

提交 form 表单数据需要提供一个 URI 编码过的 name-value 字符串。 (the section called “URIs” 介绍了 URI。)另外还要设置 Content-type 头为 application/x-www-form-urlencode

lang-dart
ch03/ch03/ch03_3_html/json-send-to-server.dart
import 'dart:html';

String encodeMap(Map data) {
  return data.keys.map((k) {
    return '${Uri.encodeComponent(k)}=${Uri.encodeComponent(data[k])}';
  }).join('&');
}

loadEnd(HttpRequest request) {
  if (request.status != 200) {
    print('Uh oh, there was an error of ${request.status}');
  } else {
    print('Data has been posted');
  }
}

main() {
  var dataUrl = '/registrations/create';
  var data = {'dart': 'fun', 'editor': 'productive'};
  var encodedData = encodeMap(data);

  var httpRequest = new HttpRequest();
  httpRequest.open('POST', dataUrl);
  httpRequest.setRequestHeader('Content-type', 
                               'application/x-www-form-urlencoded');
  httpRequest.onLoadEnd.listen((e) => loadEnd(httpRequest));
  httpRequest.send(encodedData);
}

用 WebSocket 来发送和接收实时数据

WebSocket 可以和服务器实时的交换数据,不用轮询。 服务器通过监听 ws:// 开头的 URL 来启动 WebSocket,例如 ws://127.0.0.1:1337/ws。 通过 WebSocket 可以传输 字符串、二进制数据和 一个ArrayBuffer 对象。 通常情况下使用的都是 JSON 格式的字符串。

要在 web 应用中使用 WebSocket ,先创建一个 WebSocket 对象,参数为 WebSocket URL 地址:

lang-dart
github.com/dart-lang/dart-samples/.../web/html5/websockets/basics/websocket_sample.dart
var ws = new WebSocket('ws://echo.websocket.org');

发送数据

send() 函数发送字符串:

lang-dart
github.com/dart-lang/dart-samples/.../web/html5/websockets/basics/websocket_sample.dart
ws.send('Hello from Dart!');

接收数据

在 WebSocket 上注册监听器来接收数据:

lang-dart
github.com/dart-lang/dart-samples/.../web/html5/websockets/basics/websocket_sample.dart
ws.onMessage.listen((MessageEvent e) {
  print('Received message: ${e.data}');
});

消息事件监听器接收一个 MessageEvent 对象。该对象的 data 变量包含了服务器发送的数据。

处理 WebSocket 事件

WebSocketEvents 定义了你的应用可以处理的事件: open、 close、 error、 和 (上面所演示的) message 事件。 下面是创建一个 WebSocket 对象并注册 open、 close、 error、和 message 事件的示例:

lang-dart
github.com/dart-lang/dart-samples/.../web/html5/websockets/basics/websocket_sample.dart
void initWebSocket([int retrySeconds = 2]) {
  var reconnectScheduled = false;

  print("Connecting to websocket");
  ws = new WebSocket('ws://echo.websocket.org');

  void scheduleReconnect() {
    if (!reconnectScheduled) {
      new Timer(new Duration(milliseconds: 1000 * retrySeconds), () => initWebSocket(retrySeconds * 2));
    }
    reconnectScheduled = true;
  }

  ws.onOpen.listen((e) {
    print('Connected');
    ws.send('Hello from Dart!');
  });

  ws.onClose.listen((e) {
    print('Websocket closed, retrying in $retrySeconds seconds');
    scheduleReconnect();
  });

  ws.onError.listen((e) {
    print("Error connecting to ws");
    scheduleReconnect();
  });

  ws.onMessage.listen((MessageEvent e) {
    print('Received message: ${e.data}');
  });
}

关于 WebSocket 的更多信息和示例代码请参考 Dart 代码示例。

更多信息

这节内容之上蜻蜓点水般的介绍了 dart:html 库,更多信息请参考 dart:htmlDart 代码示例。 Dart 还有一些库专门处理特殊的 web API,例如: web audio、 IndexedDB、 和 WebGL 等。

dart:io - 用于命令行应用的 I/O 操作

dart:io 库 提供了访问文件、目录、进程、socket 和 HTTP 通信的功能。 只有命令行应用才能使用 dart:io 库。

一般来说,dart:io 库的实现都是异步的。 同步操作可以很容易阻塞事件循环,只有开发可扩展 服务器应用非常困难。 因此,大多数的函数都是通过回调函数或者 Future 对象返回。

在 dart:io 库中也有少数几个以 Sync 开头的同步函数。 这里不介绍这些同步函数。

Note

只有命令行应用可以使用 dart:io

文件和目录

I/O 库可以让命令行应用读写文件和浏览目录。 有两种读取文件的方式:一次读完,或者用 stream 流读取。 一次读完一个文件需要消耗一些内存。如果文件非常大,或者你想边读取边 处理,则可以用 Stream ,在 the section called “用流的方式读取文件” 介绍了 Stream。

读取文件文本内容

当读文本文件的时候,通过函数 readAsString() 可以一次读完。 如果需要按行处理文本,则可以用 readAsLines() 函数。这两个函数都返回一个 Future 对象。

lang-dart
ch03/textRead.dart
import 'dart:io';

main() {
  var config = new File('config.txt');

  // 用一个字符串保存文件内容
  config.readAsString().then((String contents) {
    print('The entire file is ${contents.length} characters long');
  });

  // 每行文字都放到一个字符串中
  config.readAsLines().then((List<String> lines) {
    print('The entire file is ${lines.length} lines long');
  });
}

读取二进制文件

下面的代码把文件读取到一个数字 list 中。 函数 readAsBytes() 返回一个 Future。

lang-dart
ch03/binaryRead.dart
import 'dart:io';

main() {
  var config = new File('config.txt');

  config.readAsBytes().then((List<int> contents) {
    print('The entire file is ${contents.length} bytes long');
  });
}

错误处理

通过 catchError 可以处理 错误情况:

lang-dart
ch03/fileErrors.dart
import 'dart:io';

main() {
  var config = new File('config.txt');
  config.readAsString().then((String contents) {
    print(contents);
  }).catchError((e) {
    print(e);
  });
}

用流的方式读取文件

用 Stream 来一次读取一点文件内容。 listen() 函数设置一个处理函数,当有 数据可用的时候调用该函数。当 Stream 完成读取文件操作会调用 onDone 回调函数。

lang-dart
ch03/readFile.dart
import 'dart:io';
import 'dart:convert';
import 'dart:async';

main() {
  var config = new File('config.txt');
  Stream<List<int>> inputStream = config.openRead();

  inputStream
    .transform(UTF8.decoder)
    .transform(new LineSplitter())
    .listen(
      (String line) { 
        print('Got ${line.length} characters from stream');
      },
      onDone: () { print('file is now closed'); },
      onError: (e) { print(e.toString()); });
}

写文件

可以用 IOSink 来写 文件。 用 File 的 openWrite() 函数来设置读写模式和获取一个 IOSink 对象。 默认模式为 FileMode.WRITE, 该模式会覆盖文件原来的内容。

lang-dart
ch03/writeFile.dart
var logFile = new File('log.txt');
var sink = logFile.openWrite();
sink.write('FILE ACCESSED ${new DateTime.now()}\n');
sink.close();

如果要在文件末尾继续写入数据,则可以设置 mode 参数为 FileMode.APPEND

lang-dart
ch03/writeFile.dart
var sink = logFile.openWrite(mode: FileMode.APPEND); 

add(List<int> data) 来写入二进制数据。

访问目录中的文件

查找一个目录中的所有文件和子目录是一个异步操作。 list() 函数返回 一个 Stream 对象,在该对象上用 listen() 来注册文件或者目录监听器。

lang-dart
ch03/listFiles.dart
import 'dart:io';
import 'dart:async';

main() {
  var dir = new Directory('/tmp');
  var contentsStream = dir.list(recursive:true);
  contentsStream.listen(
    (FileSystemEntity f) {
      if (f is File) {
        print('Found file ${f.path}');
      } else if (f is Directory) {
        print('Found dir ${f.path}');
      }
    },
    onError: (e) { print(e.toString()); }
  );
}

其他常用功能

File 和 Directory 类还包含一些常见的功能:

  • create()函数创建文件或者目录。

  • delete() 函数删除文件或者目录。

  • length() 函数获取文件的长度。

  • open() 函数来随机访问文件。

参考 FileDirectory API 文档了解所有的函数。

HTTP 客户端和服务器

dart:io 库提供了可以访问 HTTP 资源的类。

HTTP 服务器

HttpServer 类提供了创建 web 服务器的基础功能。 你可以匹配请求、设置请求头、返回数据 等等。

下面简单的 web 服务器只能返回简单的文本内容。 该服务器监听本地(localhost 或者 127.0.0.1)的 8888 端口, 并响应路径为 /languages/dart 的请求。 所有其他的请求都是由默认的请求处理器(request handler)处理的,该处理器返回 一个 404 代码(没有发现所请求的资源)。

lang-dart
ch03/httpServer.dart
import 'dart:io';

main() {
  dartHandler(HttpRequest request) {
    request.response.headers.contentType = new ContentType('text', 'plain');
    request.response.write('Dart is optionally typed');
    request.response.close();
  };

  HttpServer.bind('127.0.0.1', 8888).then((HttpServer server) {
    server.listen((request) { 
      print('Got request for ${request.uri.path}');
      if (request.uri.path == '/languages/dart') {
        dartHandler(request);
      } else {
        request.response.write('Not found');
        request.response.close();
      }
    });
  });
}

更加完善的 HTTP 服务器,请参考 Chapter 5, 项目演练: Dartiverse Search。使用了 Dartiverse Search 示例,使用 dart:io 里面的功能 实现了一个 web 服务器,例如 http_serverroute 包。

HTTP 客户端

HttpClient 类可以访问 HTTP 资源。你可以设置请求头、使用 HTTP 方法、和 读写数据。 HttpClient 类无法在浏览器应用中使用。 在浏览器应用中需要用 HttpRequest 类。下面是一个使用 HttpClient 的示例:

lang-dart
ch03/httpClient.dart
import 'dart:io';
import 'dart:convert';

main() {
  var url = Uri.parse('http://127.0.0.1:8888/languages/dart');
  var httpClient = new HttpClient();
  httpClient.getUrl(url)
    .then((HttpClientRequest request) {
      print('have request');
      return request.close();
    })
    .then((HttpClientResponse response) {
      print('have response');
      response.transform(UTF8.decoder).toList().then((data) {
        var body = data.join('');
        print(body);
        httpClient.close();
      });
    });
}

更多信息

除了上面介绍的功能外,dart:io 还提供了 processes, sockets,web sockets 等功能。

Dart 代码示例 中 有更多介绍 dart:io 的例子。

dart:convert - 编码和解码 JSON 数据、 UTF-8 编码以及其他

dart:convert 库 提供了 JSON 和 UTF-8 编码的转换器,还支持创建自定义转换器。 JSON 是一个用来表达结构化对象和集合的文本格式。 UTF-8 是一个常用的 Unicode 字符编码。

web 应用和命令行应用都可以使用 dart:convert 库。 import dart:convert 即可使用。

编码和解码 JSON

JSON.decode() 把 JSON 文本转换为 Dart 对象:

lang-dart
ch03/jsonParse.dart
import 'dart:convert' show JSON;

main() {
  // 注意:在 JSON 字符串中需要用 双引号 来定义字符串,而不是单引号。
  var jsonString = '''
  [
    {"score": 40},
    {"score": 80}
  ]
  ''';

  var scores = JSON.decode(jsonString);
  assert(scores is List);

  var firstScore = scores[0];
  assert(firstScore is Map);
  assert(firstScore['score'] == 40);
}

使用 JSON.encode() 函数把支持 JSON 编码的对象转换为 JSON 字符串:

lang-dart
ch03/jsonStringify.dart
import 'dart:convert' show JSON;

main() {
  var scores = [
    {'score': 40},
    {'score': 80},
    {'score': 100, 'overtime': true, 'special_guest': null}
  ];

  var jsonText = JSON.encode(scores);
  assert(jsonText == '[{"score":40},{"score":80},'
                     '{"score":100,"overtime":true,'
                     '"special_guest":null}]');
}

默认只有类型为 int、 double、 String、 bool、 null、 List、 或者 Map (key 为 String) 的对象支持编码为 JSON 字符串。 List 和 Map 对象递归的编码里面的对象。

对于不支持的编码对象,你可以有两种选择: 第一种选择就是用一个方法当做第二个参数来调用 encode() 函数,该方法参数返回一个支持编码的对象;第二个选择就是忽略第二个参数,编码器会调用该对象的 toJson() 函数。

编码和解码 UTF-8 字符

使用 UTF8.decode() 来解析 UTF-8 编码格式的 字节流为 Dart string:

lang-dart
ch03/decodeUtf8.dart
import 'dart:convert' show UTF8;

main() {
  var string = UTF8.decode([0xc3, 0x8e, 0xc3, 0xb1, 0xc5, 0xa3, 0xc3, 0xa9,
                           0x72, 0xc3, 0xb1, 0xc3, 0xa5, 0xc5, 0xa3, 0xc3,
                           0xae, 0xc3, 0xb6, 0xc3, 0xb1, 0xc3, 0xa5, 0xc4,
                           0xbc, 0xc3, 0xae, 0xc5, 0xbe, 0xc3, 0xa5, 0xc5,
                           0xa3, 0xc3, 0xae, 0xe1, 0xbb, 0x9d, 0xc3, 0xb1]);
  print(string); // 'Îñţérñåţîöñåļîžåţîờñ'
}

要把 UTF-8 字符转换为一个 Dart string,则可以用 UTF8.decoder 作为 Stream 的 transform() 函数参数:

lang-dart
ch03/readFile.dart
inputStream
  .transform(UTF8.decoder)
  .transform(new LineSplitter())
  .listen(
    (String line) { 
      print('Read ${line.length} bytes from stream');
    });

UTF8.encode() 把一个 Dart string 编码为 UTF-8 格式的字节流:

lang-dart
ch03/encodeUtf8.dart
import 'dart:convert' show UTF8;

main() {
  List<int> expected = [0xc3, 0x8e, 0xc3, 0xb1, 0xc5, 0xa3, 0xc3, 0xa9, 0x72,
                        0xc3, 0xb1, 0xc3, 0xa5, 0xc5, 0xa3, 0xc3, 0xae, 0xc3,
                        0xb6, 0xc3, 0xb1, 0xc3, 0xa5, 0xc4, 0xbc, 0xc3, 0xae,
                        0xc5, 0xbe, 0xc3, 0xa5, 0xc5, 0xa3, 0xc3, 0xae, 0xe1,
                        0xbb, 0x9d, 0xc3, 0xb1];

  List<int> encoded = UTF8.encode('Îñţérñåţîöñåļîžåţîờñ');

  assert(() {
    if (encoded.length != expected.length) return false;
    for (int i = 0; i < encoded.length; i++) {
      if (encoded[i] != expected[i]) return false;
    }
    return true;
  });
}

其他功能

dart:convert 库还包含了 ASCII 和 ISO Latin 1 编码格式的转换器。 详情请参考 dart:convert 库的 API 文档。

dart:mirrors - Reflection(反射)

dart:mirrors 库提供了基础的反射功能。 用 mirror 来查询代码的结构和在运行时动态的调用函数和方法。

web 应用和命令行应用都可以使用 dart:mirrors 库。

Warning

注意: 使用 dart:mirrors 可能导致 dart2js 生成大量 的 JavaScript 文件。

当前的解决方式是在导入 dart:mirrors 之前添加一个 @MirrorsUsed 注解。详细信息请参考 MirrorsUsed API 文档。由于 dart:mirrors 库还在开发中,所有该解决方法可能会发生变化。

Symbols(符号)

mirror 系统通过 Symbol 类对象来代表 Dart 中的定义(类、变量、等等)。 Symbols 帮助编译器(例如 dart2js) 生成高效的代码。

如果在编码的时候已经知道 symbol 的名字,则可以通过 字符字面量的方式定义 symbol 对象。如果是运行时确定的名字,则可以通过 symbol 的构造函数来定义。

lang-dart
ch03/mirrors.dart
import 'dart:mirrors';

// 在编码的时候 symbol 名字已经知道
const className = #MyClass;

// 如果是动态确定名字的用构造函数
var userInput = askUserForNameOfFunction();
var functionName = new Symbol(userInput);

在压缩代码的时候,编译器科可能用一个短小的名字来替代一个 symbol 名字。 要用 symbol 还原该符号的名字,则调用 MirrorSystem.getName()。 即使代码被压缩过了,该函数还是会返回正确的名字。

lang-dart
ch03/mirrors.dart
import 'dart:mirrors';

const className = #MyClass;
assert('MyClass' == MirrorSystem.getName(className));

Introspection(审查对象)

使用 mirror 可以审查运行代码的结构。 可以审查 类、库、实例对象 等。

后面的示例使用下面的 Person 类演示:

lang-dart
ch03/mirrors.dart
class Person {
  String firstName;
  String lastName;
  int age;
  
  Person(this.firstName, this.lastName, this.age);

  String get fullName => '$firstName $lastName';

  void greet(String other) {
    print('Hello there, $other!');
  }
}

首先,需要在一个类或者对象上 reflect 一个 mirror 对象。

Class Mirrors

在一个 Type 上反射一个 ClassMirror。

lang-dart
ch03/mirrors.dart
ClassMirror mirror = reflectClass(Person);

assert('Person' == MirrorSystem.getName(mirror.simpleName));

也可以用对象的 runtimeType 从对象上获取其 Type:

lang-dart
ch03/mirrors.dart
var person = new Person('Bob', 'Smith', 33);
ClassMirror mirror = reflectClass(person.runtimeType);

assert('Person' == MirrorSystem.getName(mirror.simpleName));

一旦获取到了 ClassMirror,就可以查询类的 构造函数、变量、等信息。 下面是查询一个类的所有构造函数的示例:

lang-dart
ch03/mirrors.dart
showConstructors(ClassMirror mirror) {
  var methods = mirror.declarations.values.where((m) => m is MethodMirror);
  var constructors = methods.where((m) => m.isConstructor);
  
  constructors.forEach((m) {
    print('The constructor ${m.simpleName} has ${m.parameters.length} parameters.');
  });
}

下面是查询一个类所有变量的示例:

lang-dart
ch03/mirrors.dart
showFields(ClassMirror mirror) {
  var fields = mirror.declarations.values.where((m) => m is VariableMirror);

  fields.forEach((VariableMirror m) {
    var finalStatus = m.isFinal ? 'final' : 'not final';
    var privateStatus = m.isPrivate ? 'private' : 'not private';
    var typeAnnotation = m.type.simpleName;

    print('The field ${m.simpleName} is $privateStatus and $finalStatus and is annotated '
          'as $typeAnnotation');
  });
}

参考 ClassMirror 的 API 文档 了解所有功能。

Instance Mirrors

在对象上使用反射可以得到一个 InstanceMirror。

lang-dart
ch03/mirrors.dart
var p = new Person('Bob', 'Smith', 42);
InstanceMirror mirror = reflect(p);

如果你有个 InstanceMirror 对象,可以通过 reflectee 来获取该 InstanceMirror 对象的反射对象。

lang-dart
ch03/mirrors.dart
var person = mirror.reflectee;
assert(identical(p, person));

Invocation

一旦有了一个 InstanceMirror 对象,就可以调用其函数或者 getter 和 setter 。参考 InstanceMirror API 文档 查看所有函数。

Invoke Methods

用 InstanceMirror 的 invoke() 函数来调用 对象上的函数。第一个参数为调用的函数名字,第二个参数为该函数的参数列表, 第三个可选参数可以指定命名参数。

lang-dart
ch03/mirrors.dart
var p = new Person('Bob', 'Smith', 42);
InstanceMirror mirror = reflect(p);

mirror.invoke(#greet, ['Shailen']);

Get 和 Set Properties

用 InstanceMirror 的 getField()setField() 函数来获取和设置一个对象 的变量。

lang-dart
ch03/mirrors.dart

var p = new Person('Bob', 'Smith', 42);
InstanceMirror mirror = reflect(p);

// 获取一个变量的值
var fullName = mirror.getField(#fullName).reflectee;
assert(fullName == 'Bob Smith');
  
// 设置变量的值
mirror.setField(#firstName, 'Mary');
assert(p.firstName == 'Mary');

更多信息

Reflection in Dart with Mirrors 这篇文章介绍了反射的更多信息。 另外更详细信息请参考 dart:mirror、 MirrorsUsedClassMirror、InstanceMirror 类的 API 文档。

总结

该章介绍了 Dart 中大部分常用的内置库功能。但是该章并没有覆盖所有的库。下面这些库请参考其 API 文档:dart:collection、 dart:isolate、 dart:js、dart:typed_data。 使用 pub 工具还可以使用其他库。下一章将介绍如何用 pub 工具来引用第三方库。用 pub 来安装 args、 logging、 polymer、unittest 等库是非常简单的。