目录

Effective Dart: 使用

该指南在 Effective Dart 中是最为基础的部分。 每天在你写的 Dart 代码中都会应用到这些准则。 库的使用者可能不需要知道你在其中的一些想法, 但是维护者肯定是需要的。

这些准则可以帮助你在多个文件编写程序的情况下保证一致性和可维护性。 为了让准则简洁,这里使用“import”来同事代表 importexport 。 准则同时适用于这两者。

part of 中使用字符串。

很多 Dart 开发者会避免直接使用 part 。他们发现当库仅有一个文件的时候很容易读懂代码。 如果你确实要使用 part 将库的一部分拆分为另一个文件,则 Dart 要求另一个文件指示它所属库的路径。 由于遗留原因, Dart 允许 part of 指令使用它所属的库的名称。 这使得工具很难直接查找到这个文件对应主库文件,使得库和文件之间的关系模糊不清。

推荐的现代语法是使用 URI 字符串直接指向库文件。 首选的现代语法是使用直接指向库文件的URI字符串,URI 的使用和其他指令中一样。 如果你有一些库,my_library.dart,其中包含:

library my_library;

part "some/other/file.dart";

从库中拆分的文件应该如下所示:

part of "../../my_library.dart";

而不是:

part of my_library;

不要 导入 package 中 src 目录下的库。

lib 下的 src 目录被指定为 package 自己实现的私有库。 基于包维护者对版本的考虑,package 使用了这种约定。 在不破坏 package 的情况下,维护者可以自由地对 src 目录下的代码进行修改。

这意味着,你如果导入了其中的私有库, 按理论来讲,一个不破坏 package 的次版本就会影响到你的代码。

建议 使用相对路径在导入你自己 package 中的 lib 目录。

Linter rule: avoid_relative_lib_imports

在同一个 package 下其中一个库引用另一个 lib 目录下的库时, 应该使用相对的 URI 或者直接使用 package:

比如,下面是你的 package 目录结构:

my_package
└─ lib
   ├─ src
   │  └─ utils.dart
   └─ api.dart

如果 api.dart 想导入 utils.dart ,应该这样使用:

import 'src/utils.dart';

而不是:

import 'package:my_package/src/utils.dart';

喜欢一种方式没有什么深奥的原因——这里仅仅是因为更精简,或者是能够保持一致。

“让 package 的 lib 目录”独立分离非常重要。 lib 中的库可以导入 lib(或其子目录)中的其他库。 lib 之外的库可以使用相对导入的方式来访问lib之外的其他库。 例如,test 下可能有一个测试实用程序库被其它在 test 下的库导入。

但不能跨越导入。一个在 lib 外部的库应该永远不会引用一个在 lib 内部的库,反之亦然。 这样做,会破坏 Dart 正确判断两个库的 URL 是否引用了同一个库的能力。 遵循以下两条规则:

  • 导入路径不应包含 /lib/
  • lib 下的库永远不应该使用 ../ 来跨越 lib 目录。

字符串

下面是一些需要记住的,关于在 Dart 中使用字符串的最佳实践。

使用临近字符字的方式连接字面量字符串。

Linter rule: prefer_adjacent_string_concatenation

如果你有两个字面量字符串(不是变量,是放在引号中的字符串),你不需要使用 + 来连接它们。 应该想 C 和 C++ 一样,只需要将它们挨着在一起就可以了。 这种方式非常适合不能放到一行的长字符串的创建。

raiseAlarm(
    'ERROR: Parts of the spaceship are on fire. Other '
    'parts are overrun by martians. Unclear which are which.');
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
    'parts are overrun by martians. Unclear which are which.');

推荐 使用插值的形式来组合字符串和值。

Linter rule: prefer_interpolation_to_compose_strings

如果你之前使用过其他语言,你一定习惯使用大量 + 将字面量字符串以及字符串变量链接构建字符串。 这种方式在 Dart 中同样有效,但是通常情况下使用插值会更清晰简短。

'Hello, $name! You are ${year - birth} years old.';
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

避免 在字符串插值中使用不必要的大括号。

Linter rule: unnecessary_brace_in_string_interps

如果要插入是一个简单的标识符,并且后面没有紧跟随在其他字母文本,则应省略 {}

'Hi, $name!'
"Wear your wildest $decade's outfit."
'Wear your wildest ${decade}s outfit.'
'Hi, ${name}!'
"Wear your wildest ${decade}'s outfit."

集合

Dart 集合中原生支持了四种类型:list, map, queue, 和 set。 下面是应用于集合的最佳实践。

尽可能的使用集合字面量。

Linter rule: prefer_collection_literals

有两种方式来构造一个空的可变 list : []List() 。 同样,有三总方式来构造一个空的链表哈希 map:{}Map(), 和 LinkedHashMap()

如果想创建一个固定不变的 list 或者其他自定义集合类型,这种情况下你需要使用构造函数。 否则,使用字面量语法更加优雅。 核心库中暴露这些构造函数易于扩展,但是通常在 Dart 代码中并不使用构造函数。

var points = [];
var addresses = {};
var points = List();
var addresses = Map();

如果需要的话,你甚至可以为它们提供一个类型参数。

var points = <Point>[];
var addresses = <String, Address>{};
var points = List<Point>();
var addresses = Map<String, Address>();

注意,对于集合类的 命名 构造函数则不适用上面的规则。 List.from()Map.fromIterable() 都有其使用场景。 如果需要一个固定长度的结合,使用 List() 来创建一个固定长度的 list 也是合理的。

不要 使用 .length 来判断一个集合是否为空。

Iterable 合约并不要求集合知道其长度,也没要求在遍历的时候其长度不能改变。 通过调用 .length 来判断集合是否包含内容是非常低效的。

相反,Dart 提供了更加高效率和易用的 getter 函数:.isEmpty.isNotEmpty。 使用这些函数并不需要对结果再次取非。

if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

考虑 使用高阶(higher-order)函数来转换集合数据。

如果你有一个集合并且想要修改里面的内容转换为另外一个集合, 使用 .map().where() 以及 Iterable 提供的其他函数会让代码更加简洁。

使用这些函数替代 for 循环会让代码更加可以表述你的意图, 生成一个新的集合系列并不具有副作用。

var aquaticNames = animals
    .where((animal) => animal.isAquatic)
    .map((animal) => animal.name);

与此同时,这可以非常长, 如果你串联或者嵌套调用很多高阶函数, 则使用一些命令式代码可能会更加清晰。

避免Iterable.forEach() 中使用字面量函数。

Linter rule: avoid_function_literals_in_foreach_calls

forEach() 函数在 JavaScript 中被广泛使用, 这因为内置的 for-in 循环通常不能达到你想要的效果。 在Dart中,如果要对序列进行迭代,惯用的方式是使用循环。

for (var person in people) {
  ...
}
people.forEach((person) {
  ...
});

例外情况是,如果要执行的操作是调用一些已存在的并且将每个元素作为参数的函数, 在这种情况下,forEach() 是很方便的。

people.forEach(print);

不要 使用 List.from() 除非想修改结果的类型。

给定一个可迭代的对象,有两种常见方式来生成一个包含相同元素的 list:

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

明显的区别是前一个更短。 更重要的区别在于第一个保留了原始对象的类型参数:

// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<int>":
print(iterable.toList().runtimeType);
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);

如果你想要改变类型,那么可以调用 List.from()

var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

但是如果你的目的只是复制可迭代对象并且保留元素原始类型, 或者并不在乎类型,那么请使用 toList()

使用 whereType() 按类型过滤集合。

Linter rule: prefer_iterable_whereType

假设你有一个 list 里面包含了多种类型的对象, 但是你指向从它里面获取整型类型的数据。 那么你可以像下面这样使用 where()

var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int);

这个很罗嗦,但是更糟糕的是,它返回的可迭代对象类型可能并不是你想要的。 在上面的例子中,虽然你想得到一个 Iterable<int>,然而它返回了一个 Iterable<Object>, 这是因为,这是你过滤后得到的类型。

有时候你会看到通过添加 cast() 来“修正”上面的错误:

var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int).cast<int>();

代码冗长,并导致创建了两个包装器,获取元素对象要间接通过两层,并进行两次多余的运行时检查。 幸运的是,对于这个用例,核心库提供了 whereType()]where-type 方法:

var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType<int>();

使用 whereType() 简洁, 生成所需的 Iterable(可迭代)类型, 并且没有不必要的层级包装。

不要 使用 cast(),如果有更合适的方法。

通常,当处理可迭代对象或 stream 时, 你可以对其执行多次转换。 最后,生成所希望的具有特定类型参数的对象。 尝试查看是否有已有的转换方法来改变类型,而不是去掉用 cast() 。 而不是调用cast(),看看是否有一个现有的转换可以改变类型。

如果你已经使用了 toList() ,那么请使用 List<T>.from() 替换, 这里的 T 是你想要的返回值的类型。

var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();

如果你正在调用 map() ,给它一个显式的类型参数, 这样它就能产生一个所需类型的可迭代对象。 类型推断通常根据传递给 map() 的函数选择出正确的类型, 但有的时候需要明确指明。

var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();

避免 使用 cast()

这是对先前规则的一个宽松的定义。 有些时候,并没有合适的方式来修改对象类型,即便如此, 也应该尽可能的避免使用 cast() 来“改变”集合中元素的类型。

推荐使用下面的方式来替代:

  • 用恰当的类型创建集合。 修改集合被首次创建时的代码, 为集合提供有一个恰当的类型。

  • 在访问元素时进行 cast 操作。 如果要立即对集合进行迭代, 在迭代内部 cast 每个元素。

  • 逼不得已进行 cast,请使用 List.from() 如果最终你会使用到集合中的大部分元素, 并且不需要对象还原到原始的对象类型,使用 List.from() 来转换它。

    cast() 方法返回一个惰性集合(lazy collection),每个操作都会对元素进行检查。 如果只对少数元素执行少量操作,那么这种惰性方式就非常合适。 但在许多情况下,惰性验证和包裹(wrapping)所产生的开销已经超过了它们所带来的好处。

下面是 用恰当的类型创建集合 的示例:

List<int> singletonList(int value) {
  var list = <int>[];
  list.add(value);
  return list;
}
List<int> singletonList(int value) {
  var list = []; // List<dynamic>.
  list.add(value);
  return list.cast<int>();
}

下面是 在访问元素时进行 cast 操作 的示例:

void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (var n in objects) {
    if ((n as int).isEven) print(n);
  }
}
void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (var n in objects.cast<int>()) {
    if (n.isEven) print(n);
  }
}

下面是 使用 List.from() 进行 cast 操作 的示例:

int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = List<int>.from(objects);
  ints.sort();
  return ints[ints.length ~/ 2];
}
int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = objects.cast<int>();
  ints.sort();
  return ints[ints.length ~/ 2];
}

当然,这些替代方案并不总能解决问题,显然,这时候就应该选择 cast() 方式了。 但是考虑到这种方式的风险和缺点——如果使用不当,可能会导致执行缓慢和运行失败。

函数

在 Dart 中,就连函数也是对象。以下是一些涉及函数的最佳实践。

使用函数声明的方式为函数绑定名称。

Linter rule: prefer_function_declarations_over_variables

现代语言已经意识到本地嵌套函数和闭包的益处。 在一个函数中定义另一个函数非常常见。 在许多情况下,这些函数被立即执行并返回结果,而且不需要名字。 这种情况下非常适合使用函数表达式来实现。

但是,如果你确实需要给方法一个名字,请使用方法定义而不是把 lambda 赋值给一个变量。

void main() {
  localFunction() {
    ...
  }
}
void main() {
  var localFunction = () {
    ...
  };
}

不要 使用 lambda 表达式来替代 tear-off。

Linter rule: unnecessary_lambdas

如果你在一个对象上调用函数并省略了括号, Dart 称之为”tear-off”—一个和函数使用同样参数的闭包, 当你调用闭包的时候执行其中的函数。

如果你有一个方法,这个方法调用了参数相同的另一个方法。 那么,你不需要人为将这个方法包装到一个 lambda 表达式中。

names.forEach(print);
names.forEach((name) {
  print(name);
});

参数

使用 = 来分隔参数名和参数默认值。

Linter rule: prefer_equal_for_default_values

由于遗留原因,Dart 同时支持 := 作为参数名和默认值的分隔符。 为了与可选的位置参数保持一致,请使用 =

void insert(Object item, {int at = 0}) { ... }
void insert(Object item, {int at: 0}) { ... }

不要 显式的为参数设置 null 值。

Linter rule: avoid_init_to_null

如果你创建了一个可选参数,那么就不要为其赋默认值, Dart 默认使用 null 作为默认值,所以这里不需要为其 null 赋值语句。

void error([String message]) {
  stderr.write(message ?? '\n');
}
void error([String message = null]) {
  stderr.write(message ?? '\n');
}

变量

下面是关于如何在 Dart 中使用变量的的最佳实践。

不要 显示的为参数初始化 null 值。

在Dart中,未自动显式初始化的变量或字段将初始化为 null 。 语言保证了赋值的可靠性。在 Dart 中没有“未初始化内存”的概念。 所以使用 = null 是多余的。

int _nextId;

class LazyId {
  int _id;

  int get id {
    if (_nextId == null) _nextId = 0;
    if (_id == null) _id = _nextId++;

    return _id;
  }
}
int _nextId = null;

class LazyId {
  int _id = null;

  int get id {
    if (_nextId == null) _nextId = 0;
    if (_id == null) _id = _nextId++;

    return _id;
  }
}

避免 保存可计算的结果。

在设计类的时候,你常常希望暴露底层状态的多个表现属性。 常常你会发现在类的构造函数中计算这些属性,然后保存起来:

class Circle {
  num radius;
  num area;
  num circumference;

  Circle(num radius)
      : radius = radius,
        area = pi * radius * radius,
        circumference = pi * 2.0 * radius;
}

上面的代码有两个不妥之处。首先,这样浪费了内存。 严格来说面积和周长是缓存数据。 他们保存的结果可以通过已知的数据计算出来。 他们减少了 CPU 消耗却增加了内存消耗。 我们还没有权衡,到底存不存在性能问题?

更坏的情况是,上面的代码是 错的 。上面的缓存是无效的— 你如何知道什么时候缓存失效了需要重新计算? 在这里,我们永远不会从新计算,即使 radius 是可变的。 你可以给 radius 设置一个不同的值,但是 areacircumference 还是之前的值。

为了正确处理缓存失效,我们需要这样做:

class Circle {
  num _radius;
  num get radius => _radius;
  set radius(num value) {
    _radius = value;
    _recalculate();
  }

  num _area;
  num get area => _area;

  num _circumference;
  num get circumference => _circumference;

  Circle(this._radius) {
    _recalculate();
  }

  void _recalculate() {
    _area = pi * _radius * _radius;
    _circumference = pi * 2.0 * _radius;
  }
}

这需要编写、维护、调试以及阅读更多的代码。 如果你一开始这样写代码:

class Circle {
  num radius;

  Circle(this.radius);

  num get area => pi * radius * radius;
  num get circumference => pi * 2.0 * radius;
}

上面的代码更加简洁、使用更少的内存、减少出错的可能性。 它尽可能少的保存了表示圆所需要的数据。 这里没有字段需要同步,因为这里只有一个有效数据源。

在某些情况下,当计算结果比较费时的时候可能需要缓存, 但是只应该在你只有你有这样的性能问题的时候再去处理, 处理时要仔细,并留下挂关于优化的注释。

成员

在 Dart 中,对象成员可以是函数(方法)或数据(实例变量)。 下面是关于对象成员的最佳实践。

不要 为字段创建不必要的 getter 和 setter 方法。

Linter rule: unnecessary_getters_setters

在 Java 和 C# 中,通常情况下会将所有的字段隐藏到 getter 和 setter 方法中(在 C# 中被称为属性), 即使实现中仅仅是指向这些字段。在这种方式下,即使你在这些成员上做多少的事情,你也不需要直接访问它们。 这是因为,在 Java 中,调用 getter 方法和直接访问字段是不同的。 在 C# 中,访问属性与访问字段不是二进制兼容的。

Dart 不存在这个限制。字段和 getter/setter 是完全无法区分的。 你可以在类中公开一个字段,然后将其包装在 getter 和 setter 中, 而不会影响任何使用该字段的代码。

class Box {
  var contents;
}
class Box {
  var _contents;
  get contents => _contents;
  set contents(value) {
    _contents = value;
  }
}

推荐 使用 final 关键字来创建只读属性。

Linter rule: unnecessary_getters

如果你有一个变量,对于外部代买来说只能读取不能修改, 最简单的做法就是使用 final 关键字来标记这个变量。

class Box {
  final contents = [];
}
class Box {
  var _contents;
  get contents => _contents;
}

当然,如果你需要构造一个内部可以赋值,外部可以访问的字段, 你可以需要这种“私有成员变量,公开访问函数”的模式, 但是,如非必要,请不要使用这种模式。

考虑 对简单成员使用 =>

Linter rule: prefer_expression_function_bodies

除了使用 => 可以用作函数表达式以外, Dart 还允许使用它来定义成员。 这种风格非常适合,仅进行计算并返回结果的简单成员。

double get area => (right - left) * (bottom - top);

bool isReady(num time) => minTime == null || minTime <= time;

String capitalize(String name) =>
    '${name[0].toUpperCase()}${name.substring(1)}';

编写代码的人似乎很喜欢 => 语法,但是它很容易被滥用,最后导致代码不容易被阅读。 如果你有很多行声明或包含深层的嵌套表达式(级联和条件运算符就是常见的罪魁祸首), 你以及其他人有谁会愿意读这样的代码! 你应该换做使用代码块和一些语句来实现。

Treasure openChest(Chest chest, Point where) {
  if (_opened.containsKey(chest)) return null;

  var treasure = Treasure(where);
  treasure.addAll(chest.contents);
  _opened[chest] = treasure;
  return treasure;
}
Treasure openChest(Chest chest, Point where) =>
    _opened.containsKey(chest) ? null : _opened[chest] = Treasure(where)
      ..addAll(chest.contents);

您还可以对不返回值的成员使用 => 。 这里有个惯例,就是当 setter 和 getter 都比较简单的时候使用 =>

num get x => center.x;
set x(num value) => center = Point(value, center.y);

不要 使用 this. ,在重定向命名函数和避免冲突的情况下除外。

Linter rule: unnecessary_this

JavaScript 需要使用 this. 来引用对象的成员变量, 但是 Dart—和 C++, Java, 以及C#—没有这种限制。

只有当局部变量和成员变量名字一样的时候,你才需要使用 this. 来访问成员变量。 只有两种情况需要使用 this. 。其中一种情况是要访问的局部变量和成员变量命名一样的时候:

class Box {
  var value;

  void clear() {
    this.update(null);
  }

  void update(value) {
    this.value = value;
  }
}
class Box {
  var value;

  void clear() {
    update(null);
  }

  void update(value) {
    this.value = value;
  }
}

另一种使用 this. 的情况是在重定向到一个命名函数的时候:

class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // 这样是无法解析和编译的!
  // ShadeOfGray.alsoBlack() : black();
}
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // 现在就可以了!
  ShadeOfGray.alsoBlack() : this.black();
}

注意,构造函数初始化列表中的字段有永远不会与构造函数参数列表参数产生冲突。

class Box extends BaseBox {
  var value;

  Box(value)
      : value = value,
        super(value);
}

这看起来很令人惊讶,但是实际结果是你想要的。 幸运的是,由于初始化规则的特殊性,上面的代码很少见到。

尽可能的在定义变量的时候初始化变量值。

如果一个字段不依赖于构造函数中的参数, 则应该在定义的时候就初始化字段值。 这样可以减少需要的代码并可以确保在有多个构造函数的时候你不会忘记初始化该字段。

class Folder {
  final String name;
  final List<Document> contents;

  Folder(this.name) : contents = [];
  Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
}
class Folder {
  final String name;
  final List<Document> contents = [];

  Folder(this.name);
  Folder.temp() : name = 'temporary';
}

当然,对于变量取值依赖构造函数参数的情况以及不同的构造函数取值也不一样的情况, 则不适合本条规则。

构造函数

下面对于类的构造函数的最佳实践。

尽可能的使用初始化形式。

Linter rule: prefer_initializing_formals

许多字段直接使用构造函数参数来初始化,如:

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

为了初始化一个字段,我们需要取_四_次 x 。使用下面的方式会更好:

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

这里的位于构造函数参数之前的 this. 语法被称之为初始化形式(initializing formal)。 有些情况下这无法使用这种形式。特别是,这种形式下在初始化列表中无法看到变量。 但是如果能使用该方式,就应该尽量使用。

不要 在初始化形式中做类型注释。

Linter rule: type_init_formals

如果构造函数参数使用 this. 的方式来初始化字段, 这时参数的类型被认为和字段类型相同。

class Point {
  int x, y;
  Point(this.x, this.y);
}
class Point {
  int x, y;
  Point(int this.x, int this.y);
}

; 来替代空的构造函数体 {}

Linter rule: empty_constructor_bodies

在 Dart 中,没有具体函数体的构造函数可以使用分号结尾。 (事实上,这是不可变构造函数的要求。)

class Point {
  int x, y;
  Point(this.x, this.y);
}
class Point {
  int x, y;
  Point(this.x, this.y) {}
}

不要 使用 new

Linter rule: unnecessary_new

Dart 2 new 关键字成为可选项。 即使在Dart 1中,其含义也从未明确过, 以为在工厂构造函数中,调用 new 可能并不意味着一定会返回一个新对象。

为了减少代码迁移时的痛苦, Dart 语言仍允许使用 new 关键字, 但请考在你的代码中弃用和删除 new

Widget build(BuildContext context) {
  return Row(
    children: [
      RaisedButton(
        child: Text('Increment'),
      ),
      Text('Click!'),
    ],
  );
}
Widget build(BuildContext context) {
  return new Row(
    children: [
      new RaisedButton(
        child: new Text('Increment'),
      ),
      new Text('Click!'),
    ],
  );
}

不要 冗余地使用 const

Linter rule: unnecessary_const

在表达式一定是常量的上下文中,const 关键字是隐式的,不需要写,也不应该。 这里包括:

  • 一个字面量常量集合。
  • 调用一个常量构造函数。
  • 元数据注解。
  • 一个常量声明的初始化方法。
  • switch case 表达式—— case: 中间的部分,不是 case 执行体。

(默认值并不包含在这个列表中,因为在 Dart 将来的版本中可能会在支持非常量的默认值。)

基本上,任何地方用 new 替代 const 的写法都是错的, 因为 Dart 2 中允许省略 const

const primaryColors = [
  Color("red", [255, 0, 0]),
  Color("green", [0, 255, 0]),
  Color("blue", [0, 0, 255]),
];
const primaryColors = const [
  const Color("red", const [255, 0, 0]),
  const Color("green", const [0, 255, 0]),
  const Color("blue", const [0, 0, 255]),
];

错误处理

Dart 使用异常来表示程序执行错误。 下面是关于如何捕获和抛出异常的最佳实践。

避免 使用没有 on 语句的 catch。

Linter rule: avoid_catches_without_on_clauses

没有 on 限定的 catch 语句会捕获 try 代码块中抛出的任何异常。 Pokémon exception handling 可能并不是你想要的。 你的代码是否正确的处理 StackOverflowError 或者 OutOfMemoryError 异常? 如果你使用错误的参数调用函数,你是期望调试器定位出你的错误使用情况还是, 把这个有用的 ArgumentError 给吞噬了? 由于你捕获了 AssertionError 异常, 导致所有 try 块内的 assert() 语句都失效了,这是你需要的结果吗?

答案和可能是 “no”,在这种情况下,您应该过滤掉捕获的类型。 在大多数情况下,您应该有一个 on 子句, 这样它能够捕获程序在运行时你所关注的限定类型的异常并进行恰当处理。

不要 丢弃没有使用 on 语句捕获的异常。

如果你真的期望捕获一段代码内的 所有 异常, 请在捕获异常的地方做些事情。 记录下来并显示给用户, 或者重新抛出(rethrow)异常信息,记得不要默默的丢弃该异常信息。

只在代表编程错误的情况下才抛出实现了 Error 的异常。

Error 类是所有 编码 错误的基类。当一个该类型或者其子类型, 例如 ArgumentError 对象被抛出了,这意味着是你代码中的一个 bug。 当你的 API 想要告诉调用者使用错误的时候可以抛出一个 Error 来表明你的意图。

同样的,如果一个异常表示为运行时异常而不是代码 bug, 则抛出 Error 则会误导调用者。 应该抛出核心定义的 Exception 类或者其他类型。

不要 显示的捕获 Error 或者其子类。

本条衔接上一天内容。既然 Error 表示代码中的 bug, 应该展开整个调用堆栈,暂停程序并打印堆栈跟踪,以便找到错误并修复。

捕获这类错误打破了处理流程并且代码中有 bug。 不要在这里使用错误处理代码,而是需要到导致该错误出现的地方修复你的代码。

使用 rethrow 来重新抛出捕获的异常。

Linter rule: use_rethrow_when_possible

如果你想重新抛出一个异常,推荐使用 rethrow 语句。 rethrow 保留了原来的异常堆栈信息。 而 throw 会把异常堆栈信息重置为最后抛出的位置。

try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) throw e;
  handle(e);
}
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}

异步

Dart 具有几个语言特性来支持异步编程。 下面是针对异步编程的最佳实践。

推荐 使用 async/await 而不是直接使用底层的特性。

显式的异步代码是非常难以阅读和调试的, 即使使用很好的抽象(比如 future)也是如此。 这就是为何 Dart 提供了 async/await。 这样可以显著的提高代码的可读性并且让你可以在异步代码中使用语言提供的所有流程控制语句。

Future<int> countActivePlayers(String teamName) async {
  try {
    var team = await downloadTeam(teamName);
    if (team == null) return 0;

    var players = await team.roster;
    return players.where((player) => player.isActive).length;
  } catch (e) {
    log.error(e);
    return 0;
  }
}
Future<int> countActivePlayers(String teamName) {
  return downloadTeam(teamName).then((team) {
    if (team == null) return Future.value(0);

    return team.roster.then((players) {
      return players.where((player) => player.isActive).length;
    });
  }).catchError((e) {
    log.error(e);
    return 0;
  });
}

不要 在没有有用效果的情况下使用 async

当成为习惯之后,你可能会在所有和异步相关的函数使用 async。但是在有些情况下, 如果可以忽略 async 而不改变方法的行为,则应该这么做:

Future afterTwoThings(Future first, Future second) {
  return Future.wait([first, second]);
}
Future afterTwoThings(Future first, Future second) async {
  return Future.wait([first, second]);
}

下面这些情况 async 是有用的:

  • 你使用了 await。 (这是一个很明显的例子。)

  • 你在异步的抛出一个异常。 async 然后 throwreturn new Future.error(...) 要简短很多。

  • 你在返回一个值,但是你希望他显式的使用 Future。asyncnew Future.value(...) 要简短很多。

  • 你不希望在事件循环发生事件之前执行任何代码。

Future usesAwait(Future later) async {
  print(await later);
}

Future asyncError() async {
  throw 'Error!';
}

Future asyncValue() async => 'value';

考虑 使用高阶函数来转换事件流(stream)

This parallels the above suggestion on iterables. Streams support many of the same methods and also handle things like transmitting errors, closing, etc. correctly.

避免 直接使用 Completer 。

很多异步编程的新手想要编写生成一个 future 的代码。 而 Future 的构造函数看起来并不满足他们的要求, 然后他们就发现 Completer 类并使用它:

Future<bool> fileContainsBear(String path) {
  var completer = Completer<bool>();

  File(path).readAsString().then((contents) {
    completer.complete(contents.contains('bear'));
  });

  return completer.future;
}

Completer 是用于两种底层代码的: 新的异步原子操作和集成没有使用 Future 的异步代码。 大部分的代码都应该使用 async/await 或者 Future.then(), 这样代码更加清晰并且异常处理更加容易。

Future<bool> fileContainsBear(String path) {
  return File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
Future<bool> fileContainsBear(String path) async {
  var contents = await File(path).readAsString();
  return contents.contains('bear');
}

使用 Future<T>FutureOr<T> 参数进行测试,以消除参数可能是 Object 类型的歧义。

在使用 FutureOr<T> 执行任何有用的操作之前, 通常需要做 is 检查,来确定你拥有的是 Future<T> 还是一个空的 T。 如果类型参数是某个特定类型,如 FutureOr <int>, 使用 is intis Future<int> 那种测试都可以。 两者都有效,因为这两种类型是不相交的。

但是,如果值的类型是 Object 或者可能使用 Object 实例化的类型参数,这时要分两种情况。 Future<Object> 本身继承 Object ,使用 is Objectis T , 其中 T 表示参数的类型,该参数可能是 Object 的实例, 在这种情况下,即使是 future 对象也会返回 true 。 相反,下面是确切测试 Future 的例子:

Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is Future<T>) {
    var result = await value;
    print(result);
    return result;
  } else {
    print(value);
    return value as T;
  }
}
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is T) {
    print(value);
    return value;
  } else {
    var result = await value;
    print(result);
    return result;
  }
}

在错误的示例中,如果给它传一个 Future<Object> , 它会错误地将其视为一个空的同步对象值。