目录

高效 Dart 语言指南:用法示例

目录 keyboard_arrow_down keyboard_arrow_up

每天在你写的 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 目录下的库。

Linter rule: implementation_imports

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

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

DON’T allow an import path to reach into or out of lib.

Linter rule: avoid_relative_lib_imports

A package: import lets you access a library inside a package’s lib directory without having to worry about where the package is stored on your computer. For this to work, you cannot have imports that require the lib to be in some location on disk relative to other files. In other words, a relative import path in a file inside lib can’t reach out and access a file outside of the lib directory, and a library outside of lib can’t use a relative path to reach into the lib directory. Doing either leads to confusing errors and broken programs.

For example, say your directory structure looks like this:

my_package
└─ lib
   └─ api.dart
   test
   └─ api_test.dart

And say api_test.dart imports api.dart in two ways:

import 'package:my_package/api.dart';
import '../lib/api.dart';

Dart thinks those are imports of two completely unrelated libraries. To avoid confusing Dart and yourself, follow these two rules:

  • Don’t use /lib/ in import paths.
  • Don’t use ../ to escape the lib directory.

Instead, when you need to reach into a package’s lib directory (even from the same package’s test directory or any other top-level directory), use a package: import.

import 'package:my_package/api.dart';

A package should never reach out of its lib directory and import libraries from other places in the package.

PREFER relative import paths.

Linter rule: prefer_relative_imports

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

my_package
└─ lib
   ├─ src
   │  └─ stuff.dart
   │  └─ utils.dart
   └─ api.dart
   test
   │─ api_test.dart
   └─ test_utils.dart

Here is how the various libraries should import each other:

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

import 'src/stuff.dart';
import 'src/utils.dart';

lib/src/utils.dart:

import '../api.dart';
import 'stuff.dart';

test/api_test.dart:

import 'package:my_package/api.dart'; // Don't reach into 'lib'.

import 'test_utils.dart'; // Relative within 'test' is fine.

Null

DON’T explicitly initialize variables to null.

Linter rule: avoid_init_to_null

If a variable has a non-nullable type, Dart reports a compile error if you try to use it before it has been definitely initialized. If the variable is nullable, then it is implicitly initialized to null for you. There’s no concept of “uninitialized memory” in Dart and no need to explicitly initialize a variable to null to be “safe”.

Item? bestDeal(List<Item> cart) {
  Item? bestItem;

  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }

  return bestItem;
}
Item? bestDeal(List<Item> cart) {
  Item? bestItem = null;

  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }

  return bestItem;
}

DON’T use an explicit default value of null.

Linter rule: avoid_init_to_null

If you make a nullable parameter optional but don’t give it a default value, the language implicitly uses null as the default, so there’s no need to write it.

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

DON’T use true or false in equality operations

Using the equality operator to evaluate a non-nullable boolean expression against a boolean literal is redundant. It’s always simpler to eliminate the equality operator, and use the unary negation operator ! if necessary:

if (nonNullableBool) { ... }

if (!nonNullableBool) { ... }
if (nonNullableBool == true) { ... }

if (nonNullableBool == false) { ... }

To evaluate a boolean expression that is nullable, you should use ?? or an explicit != null check.

// If you want null to result in false:
if (nullableBool ?? false) { ... }

// If you want null to result in false
// and you want the variable to type promote:
if (nullableBool != null && nullableBool) { ... }
// Static error if null:
if (nullableBool) { ... }

// If you want null to be false:
if (nullableBool == true) { ... }

nullableBool == true is a viable expression, but shouldn’t be used for several reasons:

  • It doesn’t indicate the code has anything to do with null.

  • Because it’s not evidently null related, it can easily be mistaken for the non-nullable case, where the equality operator is redundant and can be removed. That’s only true when the boolean expression on the left has no chance of producing null, but not when it can.

  • The boolean logic is confusing. If nullableBool is null, then nullableBool == true means the condition evaluates to false.

The ?? operator makes it clear that something to do with null is happening, so it won’t be mistaken for a redundant operation. The logic is much clearer too; the result of the expression being null is the same as the boolean literal.

Using a null-aware operator such as ?? on a variable inside a condition doesn’t promote the variable to a non-nullable type. If you want the variable to be promoted inside the body of the if statement, it’s better to use an explicit != null check instead of ??.

AVOID late variables if you need to check whether they are initialized.

Dart offers no way to tell if a late variable has been initialized or assigned to. If you access it, it either immediately runs the initializer (if it has one) or throws an exception. Sometimes you have some state that’s lazily initialized where late might be a good fit, but you also need to be able to tell if the initialization has happened yet.

Although you could detect initialization by storing the state in a late variable and having a separate boolean field that tracks whether the variable has been set, that’s redundant because Dart internally maintains the initialized status of the late variable. Instead, it’s usually clearer to make the variable non-late and nullable. Then you can see if the variable has been initialized by checking for null.

Of course, if null is a valid initialized value for the variable, then it probably does make sense to have a separate boolean field.

CONSIDER assigning a nullable field to a local variable to enable type promotion.

Checking that a nullable variable is not equal to null promotes the variable to a non-nullable type. That lets you access members on the variable and pass it to functions expecting a non-nullable type. Unfortunately, promotion is only sound for local variables and parameters, so fields and top-level variables aren’t promoted.

One pattern to work around this is to assign the field’s value to a local variable. Null checks on that variable do promote, so you can safely treat it as non-nullable.

class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    final response = this.response;
    if (response != null) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }

    return 'Could not upload (no response).';
  }
}

Assigning to a local variable can be cleaner and safer than using ! every place the field or top-level variable is used:

class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    if (response != null) {
      return 'Could not complete upload to ${response!.url} '
          '(error code ${response!.errorCode}): ${response!.reason}.';
    }

    return 'Could not upload (no response).';
  }
}

Be careful when using a local variable. If you need to write back to the field, make sure that you don’t write back to the local variable instead. (Making the local variable final can prevent such mistakes.) Also, if the field might change while the local is still in scope, then the local might have a stale value. Sometimes it’s best to simply use ! on the field.

字符串

下面是一些需要记住的,关于在 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...';

Note that this guideline applies to combining multiple literals and values. It’s fine to use .toString() when converting only a single object to a string.

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

Linter rule: unnecessary_brace_in_string_interps

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

var greeting = 'Hi, $name! I love your ${decade}s costume.';
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';

集合

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

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

Linter rule: prefer_collection_literals

Dart 有三种核心集合类型。List、Map 和 Set,这些类和大多数类一样,都有未命名的构造函数,但由于这些集合使用频率很高,Dart 有更好的内置语法来创建它们:

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

Note that this guideline doesn’t apply to the named constructors for those classes. List.from(), Map.fromIterable(), and friends all have their uses. (The List class also has an unnamed constructor, but it is prohibited in null safe Dart.)

Collection literals are particularly powerful in Dart because they give you access to the spread operator for including the contents of other collections, and if and for for performing control flow while building the contents:

var arguments = [
  ...options,
  command,
  ...?modeFlags,
  for (var path in filePaths)
    if (path.endsWith('.dart')) path.replaceAll('.dart', '.js')
];
var arguments = <String>[];
arguments.addAll(options);
arguments.add(command);
if (modeFlags != null) arguments.addAll(modeFlags);
arguments.addAll(filePaths
    .where((path) => path.endsWith('.dart'))
    .map((path) => path.replaceAll('.dart', '.js')));

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

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

Linter rules: prefer_is_empty, prefer_is_not_empty

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(' ');

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

Linter rule: avoid_function_literals_in_foreach_calls

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

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

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

people.forEach(print);

您可以调用 Map.forEach()。Map 是不可迭代的,所以该准则对它无效。

不要 使用 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() 方法:

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 (final 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 (final 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() {
  void localFunction() {
    ...
  }
}
void main() {
  var localFunction = () {
    ...
  };
}

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

Linter rule: unnecessary_lambdas

如果你引用了一个函数、方法或命名构造,但省略了括号,Dart 会尝试 tear-off——在调用时使用同样的参数对对应的方法创建闭包。如果你需要的仅仅是一个引用,请不要利用 lambda 手动包装。

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

var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();

// Function:
charCodes.forEach(print);

// Method:
charCodes.forEach(buffer.write);

// Named constructor:
var strings = charCodes.map(String.fromCharCode);

// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();

// Function:
charCodes.forEach((code) {
  print(code);
});

// Method:
charCodes.forEach((code) {
  buffer.write(code);
});

// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));

// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));

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

Linter rule: prefer_equal_for_default_values

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

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

变量

The following best practices describe how to best use variables in Dart.

DO follow a consistent rule for var and final on local variables.

Most local variables shouldn’t have type annotations and should be declared using just var or final. There are two rules in wide use for when to use one or the other:

  • Use final for local variables that are not reassigned and var for those that are.

  • Use var for all local variables, even ones that aren’t reassigned. Never use final for locals. (Using final for fields and top-level variables is still encouraged, of course.)

Either rule is acceptable, but pick one and apply it consistently throughout your code. That way when a reader sees var, they know whether it means that the variable is assigned later in the function.

避免 保存可计算的结果。

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

class Circle {
  double radius;
  double area;
  double circumference;

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

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

更糟糕的是,代码是错误的。问题在于缓存是无效的 —— 你如何知道缓存何时会过期并且需要重新计算?即便半径是可变的,在这里我们也永远不会这样做。你可以赋一个不同的值,但面积和周长还是以前的值,现在的值是不正确的。

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

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

  double _area = 0.0;
  double get area => _area;

  double _circumference = 0.0;
  double get circumference => _circumference;

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

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

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

class Circle {
  double radius;

  Circle(this.radius);

  double get area => pi * radius * radius;
  double 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 {
  Object? contents;
}
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

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

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

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

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

考虑 对简单成员使用 =>

Linter rule: prefer_expression_function_bodies

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

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

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 {
  Object? value;

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

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

  void clear() {
    update(null);
  }

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

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

class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // This won't parse or compile!
  // ShadeOfGray.alsoBlack() : black();
}
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

  // But now it will!
  ShadeOfGray.alsoBlack() : this.black();
}

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

class Box extends BaseBox {
  Object? value;

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

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

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

If a field doesn’t depend on any constructor parameters, it can and should be initialized at its declaration. It takes less code and avoids duplication when the class has multiple constructors.

class ProfileMark {
  final String name;
  final DateTime start;

  ProfileMark(this.name) : start = DateTime.now();
  ProfileMark.unnamed()
      : name = '',
        start = DateTime.now();
}
class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();

  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

Some fields can’t be initialized at their declarations because they need to reference this—to use other fields or call methods, for example. However, if the field is marked late, then the initializer can access this.

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

构造函数

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

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

Linter rule: prefer_initializing_formals

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

class Point {
  double x, y;
  Point(double x, double y)
      : x = x,
        y = y;
}

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

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

This this. syntax before a constructor parameter is called an “initializing formal”. You can’t always take advantage of it. Sometimes you want to have a named parameter whose name doesn’t match the name of the field you are initializing. But when you can use initializing formals, you should.

DON’T use late when a constructor initializer list will do.

Sound null safety requires Dart to ensure that a non-nullable field is initialized before it can be read. Since fields can be read inside the constructor body, this means you get an error if you don’t initialize a non-nullable field before the body runs.

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

class Point {
  double x, y;
  Point.polar(double theta, double radius)
      : x = cos(theta) * radius,
        y = sin(theta) * radius;
}
class Point {
  late double x, y;
  Point.polar(double theta, double radius) {
    x = cos(theta) * radius;
    y = sin(theta) * radius;
  }
}

The initializer list gives you access to constructor parameters and lets you initialize fields before they can be read. So, if it’s possible to use an initializer list, that’s better than making the field late and losing some static safety and performance.

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

Linter rule: empty_constructor_bodies

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

class Point {
  double x, y;
  Point(this.x, this.y);
}
class Point {
  double 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 子句,这样它能够捕获程序在运行时你所关注的限定类型的异常并进行恰当处理。

In rare cases, you may wish to catch any runtime error. This is usually in framework or low-level code that tries to insulate arbitrary application code from causing problems. Even here, it is usually better to catch Exception than to catch all types. Exception is the base class for all runtime errors and excludes errors that indicate programmatic bugs in the code.

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

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

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

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

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

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

Linter rule: avoid_catching_errors

本条衔接上一条的内容。既然 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<int> fastestBranch(Future<int> left, Future<int> right) {
  return Future.any([left, right]);
}
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
  return Future.any([left, right]);
}

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

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

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

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

Future<void> usesAwait(Future<String> later) async {
  print(await later);
}

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

Future<String> 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;
  }
}
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> ,它会错误地将其视为一个空的同步对象值。