目录

Effective Dart: 用法

目录 keyboard_arrow_down keyboard_arrow_up
more_horiz

您可以在 Dart 代码的主体中每天使用这些指南。您的库的用户可能无法分辨您是否已经内化了这里的思想,但它的维护者肯定会。

#

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

DO 在 part of 指令中使用字符串

#

Linter 规则:use_string_in_part_of_directives

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

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

首选语法是使用直接指向库文件的 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;

DON'T 导入其他包的 src 目录内的库

#

Linter 规则:implementation_imports

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

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

DON'T 允许导入路径到达或离开 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 目录并从包中的其他位置导入库。

PREFER 使用相对导入路径

#

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 的可能性时,才是这种情况,否则不是。

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

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

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

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

#

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

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

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

CONSIDER 使用类型提升或 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 中组合字符串时需要记住的一些最佳实践。

DO 使用相邻字符串连接字符串字面量

#

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

PREFER 使用插值来组合字符串和值

#

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() 是可以的。

AVOID 在不需要时在插值中使用花括号

#

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 开箱即用地支持四种集合类型:列表、映射、队列和集合。以下最佳实践适用于集合。

DO 尽可能使用集合字面量

#

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 类也有一个未命名的构造函数,但在 null 安全的 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_emptyprefer_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() 始终是可以的。映射不可迭代,因此此指南不适用。

除非您打算更改结果的类型,否则不要使用 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);

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

使用 whereType() 按类型筛选集合

#

Linter 规则:prefer_iterable_whereType

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

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

这很冗长,但更糟糕的是,它返回一个其类型可能不是您想要的类型可迭代对象。在此示例中,它返回 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()

#

通常,当您处理可迭代对象或流时,您会对它执行多次转换。最后,您想生成一个具有特定类型参数的对象。不要添加对 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(),请为其提供显式的类型参数,以便它生成所需类型的可迭代对象。类型推断通常会根据您传递给 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 中,甚至函数也是对象。以下是一些涉及函数的最佳实践。

DO 使用函数声明将函数绑定到名称

#

Linter 规则:prefer_function_declarations_over_variables

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

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

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

DON'T 在可以使用 tear-off 时创建 lambda

#

Linter 规则:unnecessary_lambdas

当您引用一个没有括号的函数、方法或命名构造函数时,Dart 会创建一个拆分。这是一个闭包,它采用与函数相同的参数,并在您调用它时调用基础函数。如果您的代码需要一个闭包,该闭包使用与闭包接受的参数相同的参数调用命名函数,则不要将调用包装在 lambda 中。请使用拆分。

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 时,他们就知道这是否意味着该变量在函数中稍后分配。

AVOID 存储可以计算的内容

#

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

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 中,对象具有成员,这些成员可以是函数(方法)或数据(实例变量)。以下最佳实践适用于对象的成员。

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

#

Linter 规则:unnecessary_getters_setters

在 Java 和 C# 中,通常会将所有字段隐藏在 getter 和 setter(或 C# 中的属性)后面,即使实现只是转发到该字段。这样,如果您需要在这些成员中执行更多工作,您也可以这样做,而无需触及调用站点。这是因为调用 getter 方法与访问 Java 中的字段不同,并且访问属性与访问 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;
  }
}

首选使用 final 字段创建只读属性

#

如果您有一个外部代码应该能够看到但不应该赋值的字段,则一个在许多情况下都适用的简单解决方案是简单地将其标记为 final

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

当然,如果您需要在构造函数外部在内部为该字段赋值,您可能需要执行“私有字段,公共 getter”模式,但在您需要之前不要这样做。

考虑对简单成员使用 =>

#

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

除非重定向到命名构造函数或避免阴影,否则请不要使用 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);
}

这看起来令人惊讶,但它的工作方式与您期望的一样。幸运的是,由于初始化形式和超级初始化程序,这样的代码相对较少。

DO 尽可能在声明时初始化字段

#

如果字段不依赖于任何构造函数参数,则可以在声明时初始化它。它需要更少的代码,并在类具有多个构造函数时避免重复。

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

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

构造函数

#

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

DO 尽可能使用初始化形参

#

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. 语法称为“初始化形式”。您不能总是利用它。有时,您希望有一个命名参数,其名称与您要初始化的字段的名称不匹配。但是,当您可以使用初始化形式时,您应该使用它们。

当构造函数初始化列表可以完成时,请不要使用 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 并失去一些静态安全性和性能更好。

对于空的构造函数主体,请使用 ; 而不是 {}

#

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) {}
}

请不要使用 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!'),
    ],
  );
}

不要冗余地使用 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 使用异常。以下最佳实践适用于捕获和抛出异常。

避免没有 on 子句的 catch 语句

#

Linter 规则:avoid_catches_without_on_clauses

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

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

在极少数情况下,你可能希望捕获任何运行时错误。这通常是在框架或底层代码中,试图隔离任意应用程序代码,避免引起问题。即使在这里,通常最好捕获 Exception 而不是捕获所有类型。Exception 是所有运行时错误的基础类,不包括指示代码中程序性错误的错误。

不要丢弃来自没有 on 子句的 catch 语句的错误

#

如果你真的觉得需要捕获从代码区域抛出的所有异常,那么对你捕获的内容做一些事情。记录它,将其显示给用户或重新抛出它,但不要默默地丢弃它。

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

#

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

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

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

#

Linter 规则:avoid_catching_errors

这遵循上述规则。由于 Error 指示代码中的错误,它应该展开整个调用堆栈,暂停程序,并打印堆栈跟踪,以便你可以找到并修复该错误。

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

使用 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 有一些语言特性来支持异步编程。以下最佳实践适用于异步编码。

PREFER 使用 async/await 而不是原始的 future

#

异步代码是出了名的难以阅读和调试,即使使用像 futures 这样的良好抽象也是如此。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;
  });
}

在没有实际效果时不要使用 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';

CONSIDER 使用高阶方法转换 stream

#

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

AVOID 直接使用 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');
}

当消除 FutureOr<T> 的歧义时,如果其类型参数可能是 Object,则测试 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>,它会错误地将其视为裸的同步值。