跳到主要内容
目录

Effective Dart:用法

目录 keyboard_arrow_down keyboard_arrow_up
more_horiz

您可以在日常 Dart 代码中使用这些指南。您的库的用户可能无法看出您已经内化了这里的想法,但它的维护者肯定会。

#

这些指南帮助您以一致且可维护的方式从多个文件组成程序。为了保持这些指南的简洁性,它们使用“import”来涵盖 importexport 指令。这些指南同样适用于两者。

应该在 part of 指令中使用字符串

#

Linter 规则:use_string_in_part_of_directives

许多 Dart 开发者避免完全使用 part。他们发现当每个库都是单个文件时,更容易理解他们的代码。如果您确实选择使用 part 将库的一部分拆分到另一个文件中,Dart 要求另一个文件反过来指示它属于哪个库。

Dart 允许 part of 指令使用库的名称。命名库是一个遗留功能,现在不鼓励使用。当确定部件属于哪个库时,库名称可能会引入歧义。

首选语法是使用 URI 字符串,该字符串直接指向库文件。如果您有一个库 my_library.dart,其中包含

my_library.dart
dart
library my_library;

part 'some/other/file.dart';

那么 part 文件应该使用库文件的 URI 字符串

dart
part of '../../my_library.dart';

而不是库名称

dart
part of my_library;

不要导入位于另一个包的 src 目录内的库

#

Linter 规则:implementation_imports

lib 下的 src 目录被指定包含包自身实现私有的库。包维护者对其包进行版本控制的方式考虑了这一约定。他们可以自由地对 src 下的代码进行重大更改,而不会对包造成破坏性更改。

这意味着如果您导入其他包的私有库,该包的次要的、理论上非破坏性的点发布可能会破坏您的代码。

不要让导入路径进入或超出 lib

#

Linter 规则:avoid_relative_lib_imports

package: 导入允许您访问包的 lib 目录内的库,而无需担心包存储在您计算机上的哪个位置。为了使其正常工作,您不能有需要 lib 相对于其他文件位于磁盘上某个位置的导入。换句话说,lib 内文件中的相对导入路径无法访问 lib 目录之外的文件,并且 lib 目录之外的库无法使用相对路径进入 lib 目录。这样做会导致令人困惑的错误和损坏的程序。

例如,假设您的目录结构如下所示

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

假设 api_test.dart 以两种方式导入 api.dart

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

Dart 认为这些是对两个完全不相关的库的导入。为了避免混淆 Dart 和您自己,请遵循以下两个规则

  • 不要在导入路径中使用 /lib/
  • 不要使用 ../ 来转义 lib 目录。

相反,当您需要访问包的 lib 目录时(即使从同一包的 test 目录或任何其他顶级目录),请使用 package: 导入。

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

包永远不应超出lib 目录并从包中的其他位置导入库。

优先使用相对导入路径

#

Linter 规则:prefer_relative_imports

当先前的规则不起作用时,请遵循此规则。当导入跨越 lib 时,优先使用相对导入。它们更短。例如,假设您的目录结构如下所示

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

以下是各个库应如何相互导入

lib/api.dart
dart
import 'src/stuff.dart';
import 'src/utils.dart';
lib/src/utils.dart
dart
import '../api.dart';
import 'stuff.dart';
test/api_test.dart
dart
import 'package:my_package/api.dart'; // Don't reach into 'lib'.

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

Null

#

不要显式将变量初始化为 null

#

Linter 规则:avoid_init_to_null

如果变量具有非空类型,如果您尝试在变量被明确初始化之前使用它,Dart 会报告编译错误。如果变量是可空的,那么它会为您隐式初始化为 null。Dart 中没有“未初始化的内存”的概念,也不需要显式地将变量初始化为 null 以“安全”。

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

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

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

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

  return bestItem;
}

不要使用 null 的显式默认值

#

Linter 规则:avoid_init_to_null

如果您使可空参数成为可选参数,但不为其提供默认值,则语言会隐式使用 null 作为默认值,因此无需编写它。

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

不要在相等运算中使用 truefalse

#

使用相等运算符来评估针对布尔字面量的非空布尔表达式是冗余的。始终可以更简单地消除相等运算符,并在必要时使用一元否定运算符 !

dart
if (nonNullableBool) {
   ...
}

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

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

要评估可空的布尔表达式,您应该使用 ?? 或显式的 != null 检查。

dart
// 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) {
   ...
}
dart
// Static error if null:
if (nullableBool) {
   ...
}

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

nullableBool == true 是一个可行的表达式,但不应使用,原因如下

  • 它没有表明代码与 null 有任何关系。

  • 因为它显然与 null 无关,所以很容易被误认为是不可空的情况,在不可空的情况下,相等运算符是冗余的,可以删除。这仅当左侧的布尔表达式没有产生 null 的机会时才成立,但在它可以产生 null 时则不成立。

  • 布尔逻辑令人困惑。如果 nullableBool 为 null,则 nullableBool == true 表示条件评估为 false

?? 运算符清楚地表明正在发生与 null 相关的事情,因此它不会被误认为是一个冗余操作。逻辑也更清晰;表达式的结果为 null 与布尔字面量相同。

在条件内部的变量上使用 null 感知运算符(例如 ??)不会将变量提升为非空类型。如果您希望在 if 语句的主体内部提升变量,则最好使用显式的 != null 检查而不是 ??

如果需要检查 late 变量是否已初始化,请避免使用 late 变量

#

Dart 没有提供任何方法来判断 late 变量是否已初始化或赋值。如果您访问它,它要么立即运行初始化程序(如果它有),要么抛出异常。有时您有一些状态是延迟初始化的,late 可能很适合,但您也需要能够判断初始化是否已经发生。

虽然您可以通过将状态存储在 late 变量中并使用一个单独的布尔字段来跟踪变量是否已设置来检测初始化,但这很冗余,因为 Dart 内部维护着 late 变量的初始化状态。相反,通常更清楚的做法是将变量设为非 late 且可空的。然后,您可以通过检查 null 来查看变量是否已初始化。

当然,如果 null 是变量的有效初始化值,那么使用单独的布尔字段可能确实有意义。

考虑使用类型提升或 null 检查模式来使用可空类型

#

检查可空变量是否不等于 null 会将变量提升为非空类型。这使您可以访问变量上的成员并将其传递给期望非空类型的函数。

但是,类型提升仅支持局部变量、参数和私有 final 字段。可以操作的值无法进行类型提升

声明成员为私有final,正如我们通常建议的那样,通常足以绕过这些限制。但是,这并不总是可行的。

一种解决类型提升限制的模式是使用null 检查模式。这同时确认了成员的值不为 null,并将该值绑定到同一基本类型的新非空变量。

dart
class UploadException {
  final Response? response;

  UploadException([this.response]);

  @override
  String toString() {
    if (this.response case var response?) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

另一种解决方法是将字段的值分配给局部变量。对该变量的 null 检查将进行提升,因此您可以安全地将其视为非空。

dart
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).';
  }
}

使用局部变量时要小心。如果您需要写回字段,请确保您没有写回局部变量。(将局部变量设为final可以防止此类错误。)此外,如果字段可能在局部变量仍在作用域内时发生更改,则局部变量可能具有过时的值。

有时最好只是在字段上使用 !。但在某些情况下,使用局部变量或 null 检查模式可能比每次需要将值视为非空时都使用 ! 更简洁、更安全

dart
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).';
  }
}

字符串

#

以下是在 Dart 中组合字符串时需要记住的一些最佳实践。

应该使用相邻字符串来连接字符串字面量

#

Linter 规则:prefer_adjacent_string_concatenation

如果您有两个字符串字面量 - 不是值,而是实际的带引号的字面量形式 - 您无需使用 + 来连接它们。就像在 C 和 C++ 中一样,只需将它们彼此相邻放置即可。这是创建不适合一行的单个长字符串的好方法。

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

优先使用插值来组合字符串和值

#

Linter 规则:prefer_interpolation_to_compose_strings

如果您来自其他语言,您习惯于使用长链的 + 来构建由字面量和其他值组成的字符串。这在 Dart 中确实有效,但使用插值几乎总是更简洁、更短

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

请注意,此指南适用于组合多个字面量和值。仅当将单个对象转换为字符串时,使用 .toString() 是可以的。

在不需要时,避免在插值中使用花括号

#

Linter 规则:unnecessary_brace_in_string_interps

如果您要插值一个简单的标识符,并且紧随其后的不是更多的字母数字文本,则应省略 {}

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

集合

#

Dart 开箱即用地支持四种集合类型:列表、映射、队列和集合。以下最佳实践适用于集合。

应该在可能的情况下使用集合字面量

#

Linter 规则:prefer_collection_literals

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

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

请注意,此指南不适用于这些类的命名构造函数。List.from()Map.fromIterable() 和朋友都有其用途。(List 类也有一个未命名构造函数,但在空安全的 Dart 中被禁止。)

集合字面量在 Dart 中特别强大,因为它们使您可以访问扩展运算符以包含其他集合的内容,以及iffor 以在构建内容时执行控制流

dart
var arguments = [
  ...options,
  command,
  ...?modeFlags,
  for (var path in filePaths)
    if (path.endsWith('.dart')) path.replaceAll('.dart', '.js'),
];
dart
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')),
);

不要使用 .length 来查看集合是否为空

#

Linter 规则:prefer_is_empty, prefer_is_not_empty

Iterable 约定不要求集合知道其长度或能够在恒定时间内提供它。仅调用 .length 以查看集合是否包含任何内容可能会非常慢。

相反,有更快、更易读的 getter:.isEmpty.isNotEmpty。使用不需要您否定结果的那个。

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

避免将 Iterable.forEach() 与函数字面量一起使用

#

Linter 规则:avoid_function_literals_in_foreach_calls

forEach() 函数在 JavaScript 中被广泛使用,因为内置的 for-in 循环无法执行您通常想要的操作。在 Dart 中,如果您想迭代序列,惯用的方法是使用循环。

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

请注意,此指南专门说明了“函数字面量”。如果您想在每个元素上调用某个已存在的函数,则 forEach() 很好。

dart
people.forEach(print);

另请注意,始终可以使用 Map.forEach()。Map 不可迭代,因此此指南不适用。

除非您打算更改结果的类型,否则不要使用 List.from()

#

给定一个 Iterable,有两种明显的方法可以生成一个包含相同元素的新 List

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

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

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

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

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

如果您想要更改类型,那么调用 List.from() 很有用

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

但如果您的目标只是复制 iterable 并保留其原始类型,或者您不关心类型,则使用 toList()

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

#

Linter 规则:prefer_iterable_whereType

假设您有一个包含对象混合的列表,并且您只想从中获取整数。您可以像这样使用 where()

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

这很冗长,但更糟糕的是,它返回一个 iterable,其类型可能不是您想要的。在此示例中,它返回 Iterable<Object>,即使您可能想要 Iterable<int>,因为那是您要过滤到的类型。

有时您会看到通过添加 cast() 来“纠正”上述错误的代码

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

这很冗长,并且会导致创建两个包装器,具有两层间接和冗余的运行时检查。幸运的是,核心库具有whereType() 方法,用于此确切用例

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

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

当附近的运算可以完成时,不要使用 cast()

#

通常,当您处理 iterable 或 stream 时,您会对它执行多个转换。最后,您想要生成一个具有特定类型参数的对象。与其附加对 cast() 的调用,不如看看是否可以更改现有转换之一的类型。

如果您已经调用 toList(),请将其替换为对 List<T>.from() 的调用,其中 T 是您想要的生成的列表的类型。

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

如果您正在调用 map(),请为其提供显式的类型参数,以便它生成所需类型的 iterable。类型推断通常会根据您传递给 map() 的函数为您选择正确的类型,但有时您需要显式指定。

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

避免使用 cast()

#

这是先前规则的更温和的概括。有时没有附近的运算可以用来修复某些对象的类型。即使这样,在可能的情况下,也应避免使用 cast() 来“更改”集合的类型。

首选以下任何选项

  • 使用正确的类型创建它。 更改首次创建集合的代码,使其具有正确的类型。

  • 在访问时转换元素。 如果您立即迭代集合,请在迭代内部转换每个元素。

  • 使用 List.from() 急切地转换。 如果您最终将访问集合中的大多数元素,并且您不需要对象由原始实时对象支持,请使用 List.from() 转换它。

    cast() 方法返回一个惰性集合,该集合在每个操作上检查元素类型。如果您仅对少数元素执行少量操作,则惰性可能是好的。但在许多情况下,惰性验证和包装的开销超过了好处。

以下是使用正确的类型创建它的示例:

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

以下是在访问时转换每个元素

dart
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);
  }
}
dart
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() 急切地转换

dart
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];
}
dart
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 规则:prefer_function_declarations_over_variables

现代语言已经意识到局部嵌套函数和闭包的实用性。在一个函数内部定义另一个函数是很常见的。在许多情况下,此函数立即用作回调,并且不需要名称。函数表达式非常适合这种情况。

但是,如果您确实需要为其命名,请使用函数声明语句而不是将 lambda 绑定到变量。

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

当 tear-off 可以完成时,不要创建 lambda

#

Linter 规则:unnecessary_lambdas

当您在没有括号的情况下引用函数、方法或命名构造函数时,Dart 会创建一个 tear-off。这是一个闭包,它接受与函数相同的参数,并在您调用它时调用底层函数。如果您的代码需要一个闭包来调用具有与闭包接受的参数相同的参数的命名函数,请不要将调用包装在 lambda 中。使用 tear-off。

dart
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);
dart
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));

变量

#

以下最佳实践描述了如何在 Dart 中最好地使用变量。

应该在局部变量上遵循 varfinal 的一致规则

#

大多数局部变量不应具有类型注释,并且应仅使用 varfinal 声明。以下是两种广泛使用的规则,用于何时使用其中一个或另一个

  • 对于未重新赋值的局部变量使用 final,对于重新赋值的局部变量使用 var

  • 对所有局部变量使用 var,即使是那些未重新赋值的变量。永远不要对局部变量使用 final。(当然,仍然鼓励对字段和顶级变量使用 final。)

任何一个规则都是可以接受的,但请选择一个并在整个代码中一致地应用它。这样,当读者看到 var 时,他们就知道这是否意味着该变量在函数中稍后会被赋值。

避免存储可以计算的内容

#

在设计类时,您通常希望公开对同一底层状态的多个视图。通常,您会看到代码在构造函数中计算所有这些视图,然后存储它们

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

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

此代码有两个错误之处。首先,它很可能浪费内存。严格来说,面积和周长是缓存。它们是存储的计算结果,我们可以从我们已经拥有的其他数据中重新计算出来。它们正在用增加的内存来换取减少的 CPU 使用率。我们知道我们存在值得这种权衡的性能问题吗?

更糟糕的是,代码是错误的。缓存的问题是失效 - 您如何知道缓存何时过期并且需要重新计算?在这里,我们永远不会这样做,即使 radius 是可变的。您可以分配不同的值,areacircumference 将保留其先前的值,现在是不正确的值。

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

dart
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;
  }
}

这需要编写、维护、调试和阅读大量代码。相反,您的第一个实现应该是

dart
class Circle {
  double radius;

  Circle(this.radius);

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

此代码更短,使用更少的内存,并且更不容易出错。它存储表示圆所需的最小数据量。没有字段会失去同步,因为只有一个真理来源。

在某些情况下,您可能需要缓存缓慢计算的结果,但仅在您知道存在性能问题后才这样做,请小心执行,并留下注释以解释优化。

成员

#

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

不要不必要地将字段包装在 getter 和 setter 中

#

Linter 规则:unnecessary_getters_setters

在 Java 和 C# 中,通常将所有字段隐藏在 getter 和 setter(或 C# 中的属性)之后,即使实现只是转发到该字段。这样,如果您以后需要在这些成员中执行更多工作,您也可以这样做,而无需触摸调用站点。这是因为在 Java 中调用 getter 方法与访问字段不同,并且在 C# 中访问属性与访问原始字段不是二进制兼容的。

Dart 没有此限制。字段和 getter/setter 完全无法区分。您可以在类中公开一个字段,然后在以后将其包装在 getter 和 setter 中,而无需触摸任何使用该字段的代码。

dart
class Box {
  Object? contents;
}
dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

PREFER 使用 final 字段来创建只读属性

#

如果你的字段需要让外部代码可见但不可赋值,一个在许多情况下都适用的简单方案是直接将其标记为 final

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

当然,如果需要在构造函数外部在内部赋值给字段,你可能需要使用 “私有字段,公共 getter” 模式,但在你需要之前不要急于使用它。

CONSIDER 对简单成员使用 =>

#

Linter 规则: prefer_expression_function_bodies

除了对函数表达式使用 => 之外,Dart 还允许你使用它来定义成员。这种风格非常适合仅计算并返回值的简单成员。

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

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

编写 代码的人似乎很喜欢 =>,但它很容易被滥用,最终导致代码难以阅读。如果你的声明超过几行或包含深度嵌套的表达式——级联和条件运算符是常见的罪魁祸首——为了你自己和每个必须阅读你的代码的人,请使用块体和一些语句。

dart
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;
}
dart
Treasure? openChest(Chest chest, Point where) =>
    _opened.containsKey(chest)
        ? null
        : _opened[chest] = (Treasure(where)..addAll(chest.contents));

你也可以在不返回值的成员上使用 =>。当 setter 很小并且具有使用 => 的相应 getter 时,这是惯用的用法。

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

DON'T 除非为了重定向到命名构造函数或避免阴影,否则不要使用 this.

#

Linter 规则: unnecessary_this

JavaScript 要求显式的 this. 来引用当前正在执行其方法的对象上的成员,但 Dart——像 C++、Java 和 C# 一样——没有这种限制。

只有两种情况需要使用 this.。一种是当具有相同名称的局部变量遮蔽了你要访问的成员时

dart
class Box {
  Object? value;

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

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

  void clear() {
    update(null);
  }

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

另一种使用 this. 的情况是重定向到命名构造函数时

dart
class ShadeOfGray {
  final int brightness;

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

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

  ShadeOfGray(int val) : brightness = val;

  ShadeOfGray.black() : this(0);

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

请注意,构造函数参数永远不会在构造函数初始化列表中遮蔽字段

dart
class Box extends BaseBox {
  Object? value;

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

这看起来令人惊讶,但其工作方式正是你想要的。幸运的是,由于初始化形式参数和超类初始化器,这样的代码相对罕见。

应该在声明时初始化字段(如果可能)

#

如果字段不依赖于任何构造函数参数,则应该在其声明时初始化它。当类有多个构造函数时,这可以减少代码并避免重复。

dart
class ProfileMark {
  final String name;
  final DateTime start;

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

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

某些字段无法在其声明时初始化,因为它们需要引用 this——例如,使用其他字段或调用方法。但是,如果字段标记为 late,则初始化器可以访问 this

当然,如果字段依赖于构造函数参数,或者由不同的构造函数以不同的方式初始化,则此准则不适用。

构造函数

#

以下最佳实践适用于声明类的构造函数。

应该在可能的情况下使用初始化形参

#

Linter 规则: prefer_initializing_formals

许多字段直接从构造函数参数初始化,例如

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

我们在这里必须键入 x 次才能定义一个字段。我们可以做得更好

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

构造函数参数之前的 this. 语法称为 “初始化形式参数”。你并非总是可以利用它。有时你想要一个命名参数,其名称与你要初始化的字段的名称不匹配。但是,当你可以使用初始化形式参数时,你应该使用。

DON'T 当构造函数初始化列表可以完成时,不要使用 late

#

Dart 要求你在读取非可空字段之前对其进行初始化。由于可以在构造函数体内部读取字段,这意味着如果你在主体运行之前未初始化非可空字段,则会收到错误。

你可以通过将字段标记为 late 来消除此错误。如果在你初始化字段之前访问该字段,这会将编译时错误转换为运行时错误。这在某些情况下是你需要的,但通常正确的修复方法是在构造函数初始化列表中初始化字段

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

初始化列表使你可以访问构造函数参数,并允许你在读取字段之前对其进行初始化。因此,如果可以使用初始化列表,则比使字段 late 并损失一些静态安全性和性能要好。

DO 对空构造函数体使用 ; 而不是 {}

#

Linter 规则: empty_constructor_bodies

在 Dart 中,具有空主体的构造函数可以用分号终止。(实际上,const 构造函数是必需的。)

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

DON'T 使用 new

#

Linter 规则: unnecessary_new

调用构造函数时,new 关键字是可选的。它的含义不明确,因为工厂构造函数意味着 new 调用可能实际上不会返回新对象。

该语言仍然允许 new,但请将其视为已弃用并避免在你的代码中使用它。

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

DON'T 冗余地使用 const

#

Linter 规则: unnecessary_const

在表达式必须为常量的上下文中,const 关键字是隐式的,不需要编写,也不应该编写。这些上下文是内部的任何表达式

  • 常量集合字面量。
  • 常量构造函数调用
  • 元数据注解。
  • 常量变量声明的初始化器。
  • switch case 表达式——case 之后和 : 之前的部分,而不是 case 的主体。

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

基本上,在任何地方,如果编写 new 而不是 const 会导致错误,Dart 都允许你省略 const

dart
const primaryColors = [
  Color('red', [255, 0, 0]),
  Color('green', [0, 255, 0]),
  Color('blue', [0, 0, 255]),
];
dart
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 使用异常。以下最佳实践适用于捕获和抛出异常。

AVOID 避免没有 on 子句的 catch

#

Linter 规则: avoid_catches_without_on_clauses

没有 on 限定符的 catch 子句会捕获 try 块中代码抛出的任何内容。宝可梦异常处理 很可能不是你想要的。你的代码是否正确处理 StackOverflowErrorOutOfMemoryError?如果你在 try 块中错误地将错误的参数传递给方法,你是希望调试器将你指向错误,还是希望有用的 ArgumentError 被吞噬?你是否希望该代码内部的任何 assert() 语句有效地消失,因为你捕获了抛出的 AssertionError

答案可能为 “否”,在这种情况下,你应该过滤你捕获的类型。在大多数情况下,你应该有一个 on 子句,将你限制在你已知并正确处理的运行时故障类型。

在极少数情况下,你可能希望捕获任何运行时错误。这通常是在框架或低级代码中,这些代码试图将任意应用程序代码与引起问题隔离开来。即使在这里,通常最好捕获 Exception 而不是捕获所有类型。Exception 是所有运行时错误的基本类,并且排除指示代码中程序性错误的错误。

DON'T 不要丢弃来自没有 on 子句的 catch 的错误

#

如果你真的觉得需要捕获可以从代码区域抛出的所有内容,请对你捕获的内容做一些事情。记录它、向用户显示它或重新抛出它,但不要默默地丢弃它。

DO 仅对程序性错误抛出实现 Error 的对象

#

Error 类是程序性错误的基本类。当抛出该类型或其子接口之一(如 ArgumentError)的对象时,这意味着你的代码中存在 bug。当你的 API 想要向调用者报告其使用不正确时,抛出 Error 会清晰地发送该信号。

相反,如果异常是某种运行时故障,并不表示代码中存在 bug,那么抛出 Error 会产生误导。相反,抛出核心 Exception 类或其他类型之一。

DON'T 不要显式捕获 Error 或实现它的类型

#

Linter 规则: avoid_catching_errors

这遵循以上规则。由于 Error 表示你的代码中存在 bug,因此它应该展开整个调用堆栈、停止程序并打印堆栈跟踪,以便你可以找到并修复该 bug。

捕获这些类型的错误会破坏该过程并掩盖 bug。与其在事后添加错误处理代码来处理此异常,不如返回并修复导致首先抛出该异常的代码。

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

#

Linter 规则: use_rethrow_when_possible

如果你决定重新抛出异常,请首选使用 rethrow 语句,而不是使用 throw 抛出相同的异常对象。rethrow 保留异常的原始堆栈跟踪。另一方面,throw 将堆栈跟踪重置为上次抛出的位置。

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

异步

#

Dart 具有多种语言功能来支持异步编程。以下最佳实践适用于异步编码。

优先使用 async/await 而不是原始 Future

#

即使使用像 future 这样好的抽象,异步代码也以难以阅读和调试而闻名。async/await 语法提高了可读性,并允许你在异步代码中使用所有 Dart 控制流结构。

dart
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;
  }
}
dart
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;
      });
}

DON'T 当 async 没有有用的效果时不要使用它

#

很容易养成在任何与异步相关的函数上使用 async 的习惯。但在某些情况下,它是多余的。如果可以省略 async 而不更改函数的行为,请这样做。

dart
Future<int> fastestBranch(Future<int> left, Future<int> right) {
  return Future.any([left, right]);
}
dart
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
  return Future.any([left, right]);
}

async 有用 的情况包括

  • 你正在使用 await。(这是显而易见的。)

  • 你正在异步返回错误。async 然后 throwreturn Future.error(...) 更短。

  • 你正在返回值,并且希望将其隐式包装在 future 中。asyncFuture.value(...) 更短。

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

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

Future<String> asyncValue() async => 'value';

考虑使用高阶方法来转换 Stream

#

这与上面关于可迭代对象的建议类似。Streams 支持许多相同的方法,并且还可以正确处理诸如传输错误、关闭等问题。

避免直接使用 Completer

#

许多异步编程新手都想编写生成 future 的代码。Future 中的构造函数似乎不符合他们的需求,因此他们最终找到了 Completer 类并使用了它。

dart
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(),因为它们更清晰且使错误处理更容易。

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

DO 当消除类型参数可能是 ObjectFutureOr<T> 的歧义时,测试 Future<T>

#

在你对 FutureOr<T> 做任何有用的事情之前,你通常需要进行 is 检查,以查看你是否拥有 Future<T> 或裸类型 T。如果类型参数是像 FutureOr<int> 这样的特定类型,则使用哪个测试并不重要,is intis Future<int> 都可以。两者都有效,因为这两种类型是不相交的。

但是,如果值类型是 Object 或类型参数,该类型参数可能实例化为 Object,则这两个分支重叠。Future<Object> 本身实现了 Object,因此即使对象是 future,is Objectis T(其中 T 是可以实例化为 Object 的某些类型参数)也返回 true。相反,显式测试 Future 情况

dart
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;
  }
}
dart
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>,它会错误地将其视为裸同步值。