Dart 语言学习笔记

Posted by Frank on 2018-06-15

基础语法

注释

Dart 的单行注释也是 // ,注释内容最好是个完整的句子,通常以大写字母开头,以句点或其他标点符号结束:

1
2
// Not if there is nothing before it.
if (_chunks.isEmpty) return false;

/* ... */ 只适合用来注掉某一段暂时不用的代码,不适合当成注释使用。
/// 是 Dart 推荐使用的文档注释语法,dartdoc 会根据文档注释生成 HTML 文档,使用方括号可以链接到一个类、方法、字段等位置:

1
2
3
4
5
6
/// Feeds your llama [Food].
///
/// The typical llama eats one bale of hay per week.
void feed(Food food) {
// ...
}

由于历史原因, Dart 也支持 JavaDoc 样式的文档注释 /** ... */,但这种类型的文档注释会产生两个无实际内容的行,对于使用 * 标志列表项等场景并不方便,所以推荐使用 /// 文档注释。

变量

可以赋值给变量的都是对象,包括数字、函数和 null 对象,对象都是类的实例,都继承自 Object
变量存的是引用。
虽然 Dart 是强类型语言,但是类型声明是可选的,因为 Dart 可以推断出变量究竟是什么类型的。
可以使用 var 关键字声明变量而无需指定具体类型,如 var name = 'Bob';var number = 42;
有些操作需要处理任何可能的对象,如 log() 方法需要调用给定对象的 toString()。而在 Dart 中有两种类型: ObjectdynamicObject 适合表示可以是任何对象的场景,而 dynamic 适合表示更复杂的类型,如意味着 Dart 的类型系统已经不足以表示的一系列允许的类型,或者值来自 interop 或 其他超过静态类型系统范围的类型,或者你想明确地声明运行时动态处理的变量:

1
2
3
4
5
6
7
8
9
10
11
void log(Object object) {
print(object.toString());
}

/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(dynamic arg) {
if (arg is bool) return arg;
if (arg is String) return arg == 'true';
throw new ArgumentError('Cannot convert $arg to a bool.');
}

final 和 const

如果变量永远不会改变,可以使用 finalconst 去声明,final 变量只能赋值一次,const 变量是编译时常量(也是隐式 final 的),final 的顶级类变量是在其第一次被使用时初始化的。实例变量可以是 final 的,但不能是 const 的。
const 不单能声明常量,还可以创建常量值,或者声明可以创建常量值的构造器。

内置的类型

num 类型有两个子类型 intdoubleint 型位数不超过64位,在 Dart VM 上是 -263 到 263 - 1,double 是64位浮点数,遵循 IEEE 754 标准。
String 类型是 UTF-16 编码,可以使用单引号也可以使用双引号,因此可以灵活的避免字符串限定符。可以利用 ${expression} 在字符串中使用表达式的值,如果表达式是个标识符,那么 {} 可以省略。字符串拼接可以使用紧邻的字符串或者 + 号:

1
2
3
4
var s1 = 'String '
'concatenation'
" works even over line breaks.";
var s2 = 'The + operator ' + 'works, as well.';

多行的字符串可以使用三个单引号或三个双引号包裹。
可以使用前缀 r 创建原始字符串:

1
2
// 在原始字符串中,即使是 \n 也不再是特殊字符了
var s = r"In a raw string, even \n isn't special.";

Dart 的集合 API 包含 lists, sets, 和 maps。List 可以使用传统构造器创建,也可以使用 [] 语法创建,Map 可以使用传统构造器创建,也可以使用 {} 语法创建:

1
2
3
4
5
6
7
8
9
10
11
12
// Use a List constructor.
var vegetables = new List();
// Or simply use a list literal.
var fruits = ['apples', 'oranges'];
// Maps can be built from a constructor.
var searchTerms = new Map();
// Maps often use strings as keys.
var hawaiianBeaches = {
'Oahu': ['Waikiki', 'Kailua', 'Waimanalo'],
'Big Island': ['Wailea Bay', 'Pololu Beach'],
'Kauai': ['Hanalei', 'Poipu']
};

ListSet 都实现了 Iterable,可以通过迭代器去遍历,而 Map 可以通过它的keysvalues 属性获取迭代器。

函数

Dart 是真正的面向对象语言,所以即使是函数也是对象,类型是 Function。函数作为一类对象(first-class objects),可以作为参数传递给其他函数。
如果函数体只包含一个表达式语句,可以使用 => 简写,如:

1
2
3
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}

可以简写成:

1
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

这种箭头通常称作胖箭头(fat arrow)。
区分 expressionstatement 有时还是挺重要的,不包含 if 等控制流语句的程序元素都可以称之为表达式,包括操作符表达式,return 子句,throw 子句,print 等函数的调用。

可选参数

可选参数有两种,命名可选参数和有序可选参数,可选参数必须放在所有必须参数的后面。命名可选参数的列表使用 {param1, param2, …} 声明,使用 paramName: value 调用。有序可选参数的列表使用 [param1, param2, …] 声明:

1
2
3
4
5
void enableFlags(String styleName, {bool bold, bool hidden}) {
}
...
enableFlags("NORMAL", hidden: false);
`

1
2
3
4
5
6
String say(String from, String msg, [String device]) {
}
...
say('Bob', 'Howdy');
say('Bob', 'Howdy', 'smoke signal');
}

可选参数可以通过 = 指定默认参数值,这个默认参数值必须是编译时常量,如果不指定默认参数值,那么默认参数值为 null,所以不要显示地设置默认值为 null:

1
2
3
4
void enableFlags({bool bold = false, bool hidden = false}) {
}
...
enableFlags(bold: true);

main() 函数

每个 app 都必须有一个顶层 main() 函数作为 app 的入口, main() 函数的参数列表是可选的 List<String> ,返回值是 void

匿名函数

没有名字的函数有时会称为 匿名函数(anonymous function)lambda 表达式(lambda)闭包(closure),匿名函数的声明与普通函数类似:

1
2
3
([[Type] param1[, …]]) { 
codeBlock;
};

如:

1
2
3
4
var functionList = [];
functionList.add((int element) {
print("element:" + element.toString());
});

作用域

Dart 是一个 lexically scoped 语言,即 词法作用域/静态作用域 语言。也就是说,变量的作用域是根据它所处代码的布局位置静态决定的,你可以根据大括号去判断变量的作用域。

词法闭包

闭包(closure)就是一个函数对象可以访问它词法作用域内的变量,即使这个函数在原始范围外被使用。例如,下面的例子,makeAdder() 会捕获 addBy. 无论他返回的函数在哪,他都会记住 addBy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// 返回一个匿名函数,匿名函数的返回值是
/// 匿名函数的参数加上 [addBy].
Function makeAdder(num addBy) {
return (num i) => addBy + i;
}

void main() {
// 创建一个返回值是参数值加2的函数
var add2 = makeAdder(2);

// 创建一个返回值是参数值加4的函数
var add4 = makeAdder(4);

assert(add2(3) == 5);
assert(add4(3) == 7);
}

返回值

所有函数都有返回值,如果不指定返回值,默认会隐式添加 return null; 到函数体中。

操作符

5 / 2 的值是浮点数 2.5,而5 ~/ 2 的值才是整型的 2。
判断对象类型用 isis! 操作符,使用 as 操作符进行类型转换:

1
2
3
4
5
6
if (emp is Person) {
emp.firstName = 'Bob';
}
// 可以简写成下面的语句,但是如果emp 不是 Person 类型的
// 那么下面的语句会抛出异常
(emp as Person).firstName = 'Bob';

使用 ??= 操作符可以给一个值为 null 的变量赋值。使用 ?. 可以避免左边表达式的值为空导致的异常,如 foo?.barfoo 不为空时返回 bar 属性,在 foo 为空时返回 null

级联符号(..)

级联符号 .. 可以让你连续操作相同的对象,不单可以连续地调用函数,还可以连续地访问方法,这样做可以避免创建临时变量,从而写出更流畅的代码,流式编程更符合现代编程习惯和编程风格:

1
2
3
4
5
6
7
8
final addressBook = (new AddressBookBuilder()
..name = 'jenny'
..email = 'jenny@example.com'
..phone = (new PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();

严格意义上说,级联符号不是操作符,而是 Dart 语法的一部分。

循环

如果循环(迭代)的对象是 Iterable 对象,可以利用它的 forEach() 方法对集合中的元素进行操作,forEach() 方法的参数是一个函数,它会按序应用集合中的每个元素给这个函数:

1
2
3
4
5
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
list.forEach(printElement);

ListSet 这样的 Iterable 类还支持 for-in 循环:

1
2
3
4
var collection = [0, 1, 2];
for (var x in collection) {
print(x); // 0 1 2
}

异常处理

抛出的异常可以是任意对象,但最好是 ErrorException 对象:

1
2
3
throw new FormatException('Expected at least 1 section');
...
throw 'Out of llamas!';

捕获异常可以使用 oncatch 或者同时使用:

1
2
3
4
5
6
7
8
9
10
11
12
try {
breedMoreLlamas();
} on OutOfLlamasException {
// 捕获 OutOfLlamasException 类型的异常,不需要使用异常对象
buyMoreLlamas();
} on Exception catch (e) {
// 捕获 Exception 异常,打印异常对象
print('Unknown exception: $e');
} catch (e) {
// 捕获并处理任何类型的 thrown 对象
print('Something really unknown: $e');
}

catch() 可以有两个参数,第一个是抛出的异常对象,第二个是堆栈跟踪对象。如果捕获异常并处理后还允许该异常继续传播,可以使用 rethrow 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final foo = '';

void misbehave() {
try {
foo = "You can't change a final variable's value.";
} catch (e) {
print('misbehave() partially handled ${e.runtimeType}.');
rethrow; // Allow callers to see the exception.
}
}

void main() {
try {
misbehave();
} catch (e) {
print('main() finished handling ${e.runtimeType}.');
}
}

finally 子句会在 catch 子句执行完执行。如果没有 catch 子句,那么会在finally 子句运行完才开始传播异常。

类与继承

Dart 的继承是基于 mixin 的继承,也就是说,虽然每个类(除了 Object 类)有且只有一个超类,但类体仍然可以在多个类层级上重用。
构造器的名字可以是 ClassName(无名构造器),也可以是 ClassName.identifier(命名构造器),实例化时的 new 关键字可以省略。
所有的实例变量都会自动生成隐式的 getter 方法,非 final 的实例变量也会自动生成隐式的 setter 方法,使用 getset 关键词可以创建额外的属性和其 getter/setter 方法的实现。
Dart 有个语法糖可以方便地将构造器参数赋值给实例变量(在构造器体执行之前):

1
2
3
4
class Point {
num x, y;
Point(this.x, this.y);
}

如果不声明构造器,会隐式包含一个无名无参构造器,并调用父类的无名无参构造器。
子类不能继承父类的构造器,所以,如果如果子类想利用父类的命名构造器创建实例,就必须实现父类的命名构造器。如果父类没有无名无参构造器,那么你就必须手动调用父类的一个构造器:

1
2
3
4
5
6
7
8
9
10
11
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}

初始化的顺序为: 初始化列表 → 父类无参构造器 → 主类无参构造器
可以在构造器函数体执行前初始化实例变量,初始化列表使用逗号隔开,初始化列表特别适合初始化 final 字段:

1
2
3
4
5
6
7
8
9
10
class Point {
final num x;
final num y;
final num distanceFromOrigin;

Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}

重定向到同类其他构造器:

1
2
3
4
5
6
7
8
9
class Point {
num x, y;

// The main constructor for this class.
Point(this.x, this.y);

// Delegates to the main constructor.
Point.alongXAxis(num x) : this(x, 0);
}

如果类的对象永远不会改变,就可以让这个对象成为编译时常量,只需要定义 const 构造器并确保所有的实例变量是 final 的:

1
2
3
4
5
6
7
8
class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);

final num x, y;

const ImmutablePoint(this.x, this.y);
}

可以使用 factory 关键词声明工厂构造器,工厂构造器不一定要创建一个新的实例,可以直接返回缓存的实例,也可以返回子类型的实例,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Logger {
final String name;
bool mute = false;

// _cache is library-private, thanks to
// the _ in front of its name.
static final Map<String, Logger> _cache =
<String, Logger>{};

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

Logger._internal(this.name);

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

可以使用 abstract 修饰符声明一个不能直接实例化的抽象类,抽象类可以包含抽象方法,抽象方法没有方法体,以分号结尾。
每个类都隐式地定义了一个接口,该接口包含该类的所有实例成员及其实现的所有接口。可以通过 implements 子句实现一或多个接口,以逗号隔开。
使用 extends 创建子类,使用 super 引用父类,使用 @override 注解有意地重载成员。

在面向对象的语言中,多继承是个非常糟糕的设计,因为多继承会产生一系列问题,如:
多继承会使类之间的关系更加复杂。如果一个类有多个超类,超类又可能有多个超类,那么类之间的关系将会变得尤为复杂,你很难理清一个类的超类到底有哪些,继承关系又是怎样的。
多继承可能会出现菱形继承的情况,这种情况下会出现二义性,虽然 C++ 使用虚继承的方式可以暂时避免这种二义性,但会使程序可读性和可调试性变差。
所以现代的面向对象语言都不会允许多继承这种情况存在,其实继承的目的无非是为了重用并拓展类的功能,而通过继承来重用类体并不是一个好的方式,首先继承会“白箱复用”,会将超类细节暴露给子类,不管子类愿不愿意。如果超类更改,其所有子类也不得不改变。而且继承在运行时的行为不易改变。
所以尽可能少用继承,多用组合。继承是 is-a 的关系,而组合是 has-a 的关系。

Mixin 的概念源于 LISP 社区,在那里是指被设计用来与各个超类一起工作的类。对 Mixin 介绍比较详细的是 Mixins in Strongtalk 这篇论文。
一个类会隐式地定义一个 mixin, mixin 被类体定义,与类和它的超类构成函数关系,类实际上就是 mixin 的应用,是将其隐式定义的 mixin 应用于其超类的结果。Mixin应用函数应用 类似,从数学上来说,一个 mixin $M$ 可以看成是超类到子类的函数,也就是说,将 $M$ 作用于超类 $S$,结果是新的 $S$ 子类,在研究资料里通常写作 $M ▷ S$。因此对于 mixin 的组合来说: $(M_1 * M_2) ▷ S = M_1 ▷ (M_2 ▷ S)$。
一个类隐式定义的 mixin 通常只会在给定超类定义处应用一次,所以为了让 mixin 能够应用不同的超类,我们需要独立于任何特定超类来声明 mixin,或者将类隐式定义的 mixin 剥离出来并在原始声明之外重用它。

Mixin 可以通过普通类定义的方式隐式定义,但要想提取 mixin 就必须要保证类不能声明构造器,这也避免了由于在继承链上传递构造器参数引起的各种并发症。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Collection<E> {
Collection<E> newInstance();
Collection<E> map((f) {
var result = newInstance();
forEach((E e) { result.add(f(e)); });
return result;
});
void forEach(void f(E element)) {
// ...
}
void add(E element) {
// ...
}
}

abstract class DOMElementList<E> = DOMList with Collection<E>;

abstract class DOMElementSet<E> = DOMSet with Collection<E>;

在这里,Collection 就是一个隐式声明了 mixin 的普通类,它没有构造器。而 DOMElementListDOMElementSet 类就是 mixin 的应用,它们被特殊的类声明格式定义,也就是通过 with 子句声明 mixin 到 父类的应用。这两个类也是抽象的,因为他们没有实现 Collection 类的抽象方法 newInstance()
也就是说,DOMElementList 就是 Collection mixin ▷ DOMList,而 DOMElementSet 就是 Collection mixin ▷ DOMSet
这样的好处是,Collection 类的代码可以在多个类层次上共享。我们这里列举了两个类层次,一个是以 DOMList 为根的,一个是以 DOMSet 为根的。我们不需要复制粘贴Collection 类的代码,而 Collection 的任何更改都会传播到这两个类层次中,这样也极大地解放了代码的维护。
上面的例子只是简单说明了一种形式的 mixin 应用,应用的结果是一个有名字的类。而另一种形式,with 子句后跟上以逗号分隔的标识符列表,所有的标识符必须表示一个类,这种形式下,多个 mixin 会被组合并应用到 extends 子句的超类中,从而产生一个匿名超类,即:

1
2
3
4
5
6
7
class DOMElementList<E> extends DOMList with Collection<E> {
DOMElementList<E> newInstance() => new DOMElementList<E>();
}

class DOMElementSet<E> extends DOMSet with Collection<E> {
DOMElementSet<E> newInstance() => new DOMElementSet<E>();
}

这里的 DOMElementList 不再是 Collection mixin ▷ DOMList 的应用,而是一个新类,它的超类是这个应用。
如果 DOMList 有非平凡构造器,那么构造将会是这样:

1
2
3
4
class DOMElementList<E> extends DOMList with Collection<E> {
DOMElementList<E> newInstance() => new DOMElementList<E>(0);
DOMElementList(size): super(size);
}

每个 mixin 都有属于自己的被独立调用的构造器,超类也是如此。由于 mixin 的构造器不能被声明,对它的调用可以用 ; 语法省略,在底层实现时,这个调用会被放在初始化列表的开始处。
为了让多个 mixin 应用到一个类是而又不引入多个中间声明,可以使用这种方便的语法糖:

1
2
3
4
5
6
7
8
class Person {
String name;
Person(this.name);
}

class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(name):super(name);
}

在这里,Maestro 的超类就是这样的 mixin 应用:
Demented mixin ▷ Aggressive mixin ▷ Musical mixin ▷ Person
mixin 与普通继承一样,隐私性取决于其声明的位置。mixin 的应用的静态元素同样不能被继承使用,Dart 中静态元素是不能被继承的。
那 mixin 应用后的实例类型到底是什么类型呢?毫无疑问,它必须是命名 mixin 类的子类型,也必须是原来类的子类型。原来的类有它自己的超类,为了确保指定的 mixin 应用 兼容原来的类,Dart 会给 with 子句的类额外的要求。
如果类 A 定义时的 with 子句是 mixin M,而 M 又是继承的 类 K,那么类 A 就必须支持类 K 的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class S {
twice(int x) => 2 * x;
}

abstract class I {
twice(x);
}

abstract class J {
thrice(x);
}
class K extends S implements I, J {
int thrice(x) => 3* x;
}

class B {
twice(x) => x + x;
}
class A = B with K;

A 必须支持 K 的超类 S 的隐式接口,以确保 A 确实是 M 的子类型。K 必须实现 twice() 以满足 I 的需要,同时必须实现 thrice() 以满足 J 的需要,显然 K 已经满足了这些需要。
现在我们声明了 A,我们从 K 的 mixin 中 拿到了 thrice() 的实现,但这个 mixin 没有提供 twice() 的实现,幸运的是,B 有这个实现,所以总的来说,A 确实满足了 I,J 和 S 的需要。
相反,对于这样的类 D:

1
2
3
4
5
class D {
double(x) => x+x;
}

class E = D with K;

会收到一个警告,因为 E 没有 twice() 方法,因此不满足 I 和 S,也就不能使用预期的 K 了。
如果一个类有泛型参数,那么它的 mixin 也必须有相同类型的参数。
在 1.13 版本之后,Mixin 可以继承普通类而不要求一定是 Object,Mixin 可以使用 super.method()

操作符重载

可以通过 operator 关键字重载包括 +>== 在内的操作符,但要注意,重载 == 操作符的同时也要重载 hashCodegetter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
final String firstName, lastName;

Person(this.firstName, this.lastName);

// Override hashCode using strategy from Effective Java,
// Chapter 11.
@override
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}

// You should generally implement operator == if you
// override hashCode.
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return (person.firstName == firstName &&
person.lastName == lastName);
}
}

枚举类型

使用 enum 关键词定义枚举类型,枚举类型的值有一个 index getter,枚举值的列表可以通过它的 values 常量获取,枚举支持 switch 语句,你不能继承,mix in 或者实现枚举类,不能显式实例化枚举:

1
2
3
enum Color { red, green, blue }
assert(Color.red.index == 0);
List<Color> colors = Color.values;

library 和 可见性

每个 Dart app 都是一个 library,即使它没使用 library 指令,library 不仅提供 API,还是个隐私单元,以下划线(_)开头的标识符只在当前 library 内可见。
使用 import 指令可以指定在另一个 library 范围内如何使用另一个 library 的命名空间。对于内置的 library,URI 是特定的 dart: scheme,而其他的 URI 可以是文件系统路径或者 package: scheme:

1
2
import 'dart:html';
import 'package:test/test.dart';

如果两个 library 的标识符冲突,可以使用前缀加以区分:

1
2
3
4
5
6
7
8
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = new Element();

// Uses Element from lib2.
lib2.Element element2 = new lib2.Element();

如果只想引入 library 的部分功能,需要使用 showhide 指定:

1
2
3
4
5
// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

如果想要懒加载某个 library,先在 import 时通过 deferred as 指定 library 的标识符,然后在需要使用它的时候调用标识符的 loadLibrary():

1
2
3
4
5
6
import 'package:greetings/hello.dart' deferred as hello;
...
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}

此时 await 关键字会暂停程序执行,直到 library 加载成功,可以多次调用 loadLibrary(),因为 library 只会加载一次。

异步

Dart 为异步提供了 FutureStream 两个类,Future 就像将来某个时刻提供结果的承诺,Stream 是获取值序列(例如事件序列)的一种方式。
很多时候没有必要直接使用 Future 或 Stream API,而是使用 asyncawait 关键字进行异步编程,这样更容易理解,而且代码看起来也更像同步代码。
await 必须在异步函数中使用,用来等待异步函数返回的结果,如:

1
2
3
4
Future checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}

虽然异步函数可以执行耗时操作,但它不用等待耗时操作完成,这个异步函数会执行直到遇到第一个 await 表达式,然后返回 Future 对象,在 await 表达式执行完后再继续执行。
可以在一个异步函数里 await 多次,也可以 try/catch await 的代码。
await expression 语句中,expression 的值通常是个 Future,如果不是,那么它会被自动包装成 Future。await expression 会暂停其之后代码的执行,直到返回的对象可用。
如果异步函数不需要返回有用值,可以那么返回类型应该是 Future<void>
可以用 await for 异步等待 stream 中的所有结果,但不能是 UI 事件流这样的 stream,因为这样的 stream 是无限的。

生成器函数

Dart 内置了两种生成器函数,同步的生成器函数返回 Iterable 对象,异步的生成器函数返回 Stream 对象,使用 sync*/async* 指定同步异步,使用 yield 交付值,还可以通过 yield* 递归生成:

1
2
3
4
5
6
7
8
9
10
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
Iterable<int> naturalsDownFrom(int n) sync* {
if (n > 0) {
yield n;
yield* naturalsDownFrom(n - 1);
}
}

注解

自定义注解就像声明普通类一样:

1
2
3
4
5
6
7
8
library todo;

class Todo {
final String who;
final String what;

const Todo(this.who, this.what);
}

Effective Dart

代码风格

  • 类、枚举、注解、typedef 要使用大驼峰命名法。
  • library 和 源文件的命名(因为有些文件系统并不是大小写敏感的)、import 前缀(即 as 子句)要全部小写并用下划线分隔。
  • 常量名等要用小驼峰命名法。
  • 除了两个字母的首字母缩写词,其他的首字母缩写词和缩写词应该和普通单词一样进行标识符大小写。如:

    1
    2
    3
    4
    5
    6
    HttpConnectionInfo
    uiHandler
    IOStream
    HttpRequest
    Id
    DB
  • 每行尽量不超过80个字符。

    使用方式

  • 引入自己库 lib 目录下的 library时,要使用相对路径。
  • 一行放不下的字符串不要使用加号拼接,直接相邻两行书写即可。对于表达式的值和其他字符串组合的字符串,也不要使用加号拼接,直接使用 ${expression} 插值即可。
  • 使用 .length 判断集合是否为空可能会非常慢,所以使用 .isEmpty.isNotEmpty 判断集合情况是最好的选择,更快也更直观。
  • 拷贝集合有两种方式:

    1
    2
    var copy1 = iterable.toList();
    var copy2 = new List.from(iterable);

    第一种可以保存原始对象的类型参数,而第二种的运行时类型是 dynamic 的,不过可以通过 new List<int>.from(numbers) 这样的方式指定。

  • 过滤集合中的某类元素时应该使用 whereType():

    1
    2
    var objects = [1, "a", 2, "b", 3];
    var ints = objects.whereType<int>();
  • 转换 iterable 或 stream 的类型时,尽量不要用 cast(),因为该方法会返回 lazy 集合并且每次操作都要检查元素类型,如果集合元素和元素操作特别多那么 lazy 验证和打包的开销会特别大,所以 stuff.toList().cast<int>() 完全可以用 new List<int>.from(stuff) 替代,stuff.map((n) => 1 / n).cast<double>() 完全可以用 stuff.map<double>((n) => 1 / n) 替代。

  • 善于使用 => 简化简单的代码逻辑,如:
    1
    2
    3
    num get x => center.x;
    set x(num value) => center = new Point(value, center.y);
    bool isReady(num time) => minTime == null || minTime <= time;

参考