“Effective Dart:用法”
您可以每天在 Dart 代码体中使用这些准则。您的库的 用户 可能无法知道您已经内化了这里的想法,但它的 维护者 肯定知道。
库
#这些准则帮助您以一致且可维护的方式将程序组合成多个文件。为了使这些准则简短,它们使用“import”来涵盖 import
和 export
指令。这些准则同样适用于两者。
应在 part of
指令中使用字符串
#Linter 规则:
许多 Dart 开发人员完全避免使用 part
。当每个库都是单个文件时,他们发现更容易理解他们的代码。如果您确实选择使用 part
将库的一部分拆分到另一个文件中,Dart 要求另一个文件反过来指示它是哪个库的一部分。
Dart 允许 part of
指令使用库的 名称 。命名库是一个现在 不推荐 的遗留功能。库名称在确定部分属于哪个库时可能会导致歧义。
首选语法是使用指向库文件的 URI 字符串。如果您有一些库 my_library.dart
,其中包含:
library my_library;
part 'some/other/file.dart';
那么部分文件应该使用库文件的 URI 字符串:
part of '../../my_library.dart';
而不是库名称:
part of my_library;
不要导入另一个包的 src
目录内的库
#Linter 规则:
lib
下的 src
目录 指定 用于包含对包自身实现私有的库。包维护者对其包进行版本控制的方式考虑到了此约定。他们可以自由地对 src
下的代码进行大规模更改,而不会对包造成重大更改。
这意味着,如果您导入其他包的私有库,则该包的次要(理论上非重大)点版本发布可能会破坏您的代码。
不要允许导入路径进入或超出 lib
#Linter 规则:
package:
导入允许您访问包 lib
目录内的库,而无需担心包存储在计算机上的位置。为了使此方法有效,您不能使用需要 lib
相对于其他文件位于磁盘上的某个位置的导入。换句话说, lib
内的文件中的相对导入路径不能访问 lib
目录外的文件,并且 lib
目录外的库不能使用相对路径进入 lib
目录。执行任何一项操作都会导致令人困惑的错误和程序中断。
例如,假设您的目录结构如下所示:
my_package
└─ lib
└─ api.dart
test
└─ api_test.dart
假设 api_test.dart
以两种方式导入 api.dart
:
import 'package:my_package/api.dart';
import '../lib/api.dart';
Dart 认为这些是两个完全无关的库的导入。为了避免混淆 Dart 和您自己,请遵循以下两条规则:
- 不要在导入路径中使用
/lib/
。 - 不要使用
../
来跳出lib
目录。
相反,当您需要进入包的 lib
目录时(即使是从同一个包的 test
目录或任何其他顶级目录),请使用 package:
导入。
import 'package:my_package/api.dart';
包不应该超出其 lib
目录并从包中的其他位置导入库。
首选相对导入路径
#Linter 规则:
每当之前的规则不起作用时,请遵循此规则。当导入 不 跨越 lib
时,请首选使用相对导入。它们更短。例如,假设您的目录结构如下所示:
my_package
└─ lib
├─ src
│ └─ stuff.dart
│ └─ utils.dart
└─ api.dart
test
│─ api_test.dart
└─ test_utils.dart
以下是各个库相互导入的方式:
import 'src/stuff.dart';
import 'src/utils.dart';
import '../api.dart';
import 'stuff.dart';
import 'package:my_package/api.dart'; // 不要进入 'lib'。
import 'test_utils.dart'; // 'test' 内的相对路径是可以的。
Null
#不要显式地将变量初始化为 null
#Linter 规则:
如果变量具有不可为空类型,则如果您尝试在变量被明确初始化之前使用它,Dart 会报告编译错误。如果变量是可空的,则它会为您隐式初始化为 null
。Dart 中没有“未初始化内存”的概念,因此无需显式地将变量初始化为 null
以“安全”。
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;
}
不要使用 null
的显式默认值
#Linter 规则:
如果您使可空参数可选但没有为其提供默认值,则该语言会隐式使用 null
作为默认值,因此无需编写它。
void error([String? message]) {
stderr.write(message ?? '\n');
}
void error([String? message = null]) {
stderr.write(message ?? '\n');
}
不要在等式运算中使用 true
或 false
#使用等式运算符将 不可为空的 布尔表达式与布尔文字进行比较是多余的。消除等式运算符并根据需要使用一元非运算符 !
始终更简单:
if (nonNullableBool) { ... }
if (!nonNullableBool) { ... }
if (nonNullableBool == true) { ... }
if (nonNullableBool == false) { ... }
要评估 可为空的 布尔表达式,应使用 ??
或显式 != null
检查。
// 如果希望 null 导致 false:
if (nullableBool ?? false) { ... }
// 如果希望 null 导致 false
// 并且希望变量进行类型提升:
if (nullableBool != null && nullableBool) { ... }
// 如果为 null,则为静态错误:
if (nullableBool) { ... }
// 如果希望 null 为 false:
if (nullableBool == true) { ... }
nullableBool == true
是一个可行的表达式,但不应使用它,原因如下:
它没有表明代码与
null
有任何关系。因为它并非明显与
null
相关,因此很容易将其误认为不可为空的情况,在这种情况下,等式运算符是多余的,可以删除。只有当左边的布尔表达式没有机会产生null
时才如此,但当它可以产生null
时则不然。布尔逻辑令人困惑。如果
nullableBool
为 null,则nullableBool == true
表示条件计算结果为false
。
??
运算符清楚地表明正在发生与 null 相关的事情,因此不会将其误认为是多余的操作。逻辑也更加清晰;表达式的结果为 null
与布尔文字相同。
在条件内对变量使用 ??
等空感知运算符不会将变量提升为不可为空类型。如果您希望变量在 if
语句的主体中被提升,最好使用显式 != null
检查而不是 ??
。
如果需要检查变量是否已初始化,请避免使用 late
变量
#Dart 没有提供任何方法来判断 late
变量是否已初始化或已赋值。如果您访问它,它要么立即运行初始化程序(如果存在),要么抛出异常。有时您有一些延迟初始化的状态,其中 late
可能很合适,但您也需要能够 判断 初始化是否已发生。
尽管您可以通过将状态存储在 late
变量中并使用一个单独的布尔字段来跟踪变量是否已设置来检测初始化,但这仍然是多余的,因为 Dart 内部维护 late
变量的初始化状态。相反,通常更清楚的是使变量为非 late
和可空的。然后您可以通过检查 null
来查看变量是否已初始化。
当然,如果 null
是变量的有效初始化值,那么拥有一个单独的布尔字段可能确实有意义。
考虑使用类型提升或空检查模式来使用可空类型
#检查可空变量不等于 null
会将变量提升为不可为空类型。这允许您访问变量上的成员并将它们传递给期望不可为空类型的函数。
但是,类型提升仅支持局部变量、参数和私有 final 字段。可以进行操作的值 不能进行类型提升 。
按照我们的常规建议,声明成员 私有 和 final 通常足以绕过这些限制。但是,这并非总是可行的。
解决类型提升限制的一种模式是使用 空检查模式 。这同时确认成员的值不为 null,并将该值绑定到具有相同基类型的新的不可为空变量。
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).';
}
}
另一种解决方法是将字段的值赋值给局部变量。对该变量进行空检查将进行提升,因此您可以安全地将其视为不可为空的。
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
可以防止此类错误。)此外,如果在局部变量仍在作用域内时字段可能会发生更改,则局部变量可能具有过时值。
有时最好只对字段使用 !
。但是,在某些情况下,使用局部变量或空检查模式比每次需要将值视为非空值时都使用 !
更简洁、更安全:
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 规则:
如果您有两个字符串字面量——不是值,而是实际的带引号的字面量形式——则不需要使用 +
来连接它们。就像在 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 规则:
如果您来自其他语言,您习惯于使用很长的 +
链来从字面量和其他值构建字符串。这在 Dart 中确实有效,但使用插值几乎总是更简洁和更短:
'Hello, $name! You are ${year - birth} years old.';
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
请注意,此准则适用于组合 多个 字面量和值。在仅将单个对象转换为字符串时,使用 .toString()
就可以了。
避免在不需要时在插值中使用花括号
#Linter 规则:
如果您正在插值一个简单的标识符,该标识符后面没有紧跟更多字母数字文本,则应省略 {}
。
var greeting = 'Hi, $name! I love your ${decade}s costume.';
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';
集合
#开箱即用,Dart 支持四种集合类型:列表、映射、队列和集合。以下最佳实践适用于集合。
尽可能使用集合字面量
#Linter 规则:
Dart 有三种核心集合类型:List、Map 和 Set。Map 和 Set 类像大多数类一样具有未命名的构造函数。但是,由于这些集合使用非常频繁,因此 Dart 提供了更简洁的内置语法来创建它们:
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
var addresses = Map<String, Address>();
var counts = Set<int>();
请注意,此准则不适用于这些类的 命名 构造函数。 List.from()
、 Map.fromIterable()
及其朋友都有其用途。(List 类也具有未命名的构造函数,但在空安全 Dart 中它是禁止的。)
集合字面量在 Dart 中特别强大,因为它们使您可以访问用于包含其他集合内容的 展开运算符 ,以及用于在构建内容时执行控制流的 if
和 for
:
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')));
不要使用 .length
来查看集合是否为空
#Linter 规则:
Iterable 契约不要求集合知道其长度或能够在恒定时间内提供它。调用 .length
只是为了查看集合是否包含 任何内容 可能会非常慢。
相反,存在更快且更易读的 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 规则:
forEach()
函数在 JavaScript 中被广泛使用,因为内置的 for-in
循环不会执行您通常想要的操作。在 Dart 中,如果您想迭代一个序列,那么执行此操作的惯用方式是使用循环。
for (final person in people) {
...
}
people.forEach((person) {
...
});
请注意,此准则特别说明了“函数 字面量 ”。如果您想对每个元素调用一些 已经存在的 函数,则 forEach()
就可以了。
people.forEach(print);
另请注意,使用 Map.forEach()
始终是可以的。映射不可迭代,因此此准则不适用。
不要使用 List.from()
,除非您打算更改结果的类型
#给定一个 Iterable,有两种显而易见的方法可以生成一个包含相同元素的新 List:
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
显而易见的区别是第一个更短。 重要 的区别是第一个保留了原始对象的类型参数:
// 创建一个 List<int>:
var iterable = [1, 2, 3];
// 打印 "List<int>":
print(iterable.toList().runtimeType);
// 创建一个 List<int>:
var iterable = [1, 2, 3];
// 打印 "List<dynamic>":
print(List.from(iterable).runtimeType);
如果您 想要 更改类型,则调用 List.from()
很有用:
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // 现在它只包含整数。
var ints = List<int>.from(numbers);
但是,如果您的目标只是复制 iterable 并保留其原始类型,或者您不关心类型,则使用 toList()
。
应使用 whereType()
按类型筛选集合
#Linter 规则:
假设您有一个包含混合对象的列表,并且您只想从中获取整数。您可以像这样使用 where()
:
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);
这很冗长,但更糟糕的是,它返回的 Iterable 类型可能不是您想要的。在此示例中,它返回一个 Iterable<Object>
,即使您可能想要一个 Iterable<int>
,因为这就是您将其筛选到的类型。
有时您会看到通过添加 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()
#当您处理可迭代对象或流时,您通常会在其上执行多个转换。最后,您希望生成一个具有特定类型参数的对象。与其附加对 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()
来“更改”集合的类型。
相反,最好使用以下任何选项:
使用正确的类型创建它。 更改首次创建集合的代码,使其具有正确的类型。
访问时强制转换元素。 如果您立即迭代集合,请在迭代中强制转换每个元素。
使用
List.from()
积极强制转换。 如果您最终将访问集合中的大部分元素,并且您不需要该对象由原始活动对象支持,请使用List.from()
转换它。cast()
方法返回一个延迟集合,该集合在 每次操作 时都会检查元素类型。如果您只对少数几个元素执行少量操作,这种延迟性可能很好。但在许多情况下,延迟验证和包装的开销超过了其好处。
以下是 使用正确的类型创建它 的示例:
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>();
}
以下是 访问时强制转换每个元素 :
void printEvens(List<Object> objects) {
// 我们碰巧知道列表只包含整数。
for (final n in objects) {
if ((n as int).isEven) print(n);
}
}
void printEvens(List<Object> objects) {
// 我们碰巧知道列表只包含整数。
for (final n in objects.cast<int>()) {
if (n.isEven) print(n);
}
}
以下是 使用 List.from()
积极强制转换 :
int median(List<Object> objects) {
// 我们碰巧知道列表只包含整数。
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}
int median(List<Object> objects) {
// 我们碰巧知道列表只包含整数。
var ints = objects.cast<int>();
ints.sort();
return ints[ints.length ~/ 2];
}
当然,这些替代方法并不总是有效,有时 cast()
是正确的答案。但认为这种方法有点冒险且不可取——如果不小心,它可能会很慢并且可能在运行时失败。
函数
#在 Dart 中,即使函数也是对象。以下是一些涉及函数的最佳实践。
应使用函数声明将函数绑定到名称
#Linter 规则:
现代语言已经意识到局部嵌套函数和闭包有多么有用。在许多情况下,这个函数被立即用作回调,不需要名称。函数表达式非常适合这种情况。
但是,如果您确实需要为其命名,请使用函数声明语句,而不是将 lambda 绑定到变量。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
不要在撕裂操作可以实现时创建 lambda
#Linter 规则:
当您引用函数、方法或命名构造函数而不带括号时,Dart 会创建一个 撕裂 。这是一个闭包,它接受与函数相同的参数,并在您调用它时调用底层函数。如果您的代码需要一个闭包来调用具有与闭包接受的参数相同的参数的命名函数,请不要将调用包装在 lambda 中。使用撕裂。
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// 函数:
charCodes.forEach(print);
// 方法:
charCodes.forEach(buffer.write);
// 命名构造函数:
var strings = charCodes.map(String.fromCharCode);
// 未命名构造函数:
var buffers = charCodes.map(StringBuffer.new);
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// 函数:
charCodes.forEach((code) {
print(code);
});
// 方法:
charCodes.forEach((code) {
buffer.write(code);
});
// 命名构造函数:
var strings = charCodes.map((code) => String.fromCharCode(code));
// 未命名构造函数:
var buffers = charCodes.map((code) => StringBuffer(code));
变量
#以下最佳实践描述了如何在 Dart 中最好地使用变量。
应遵循关于局部变量的 var
和 final
的一致规则
#大多数局部变量不应具有类型注释,并且应该只使用 var
或 final
来声明。对于何时使用一个或另一个,有两种广泛使用的规则:
对于未重新赋值的局部变量使用
final
,对于已重新赋值的局部变量使用var
。对于所有局部变量使用
var
,即使是那些未重新赋值的变量。永远不要对局部变量使用final
。(当然,仍然鼓励对字段和顶级变量使用final
。)
任一规则都可以接受,但请选择 一个 并将其一致地应用于整个代码中。这样,当读者看到 var
时,他们就知道它是否意味着该变量稍后在函数中被赋值。
避免存储可以计算的内容
#在设计类时,您通常希望公开对相同底层状态的多个视图。您经常会看到在构造函数中计算所有这些视图然后存储它们的代码:
class Circle {
double radius;
double area;
double circumference;
Circle(double radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
这段代码有两个问题。首先,它很可能浪费内存。严格来说,面积和周长是 缓存 。它们是我们可以根据我们已经拥有的其他数据重新计算的存储计算。它们是用增加的内存来换取减少的 CPU 使用率。我们知道我们是否有需要这种权衡的性能问题吗?
更糟糕的是,代码是 错误的 。缓存的问题是 失效 ——您如何知道缓存何时过期并需要重新计算?在这里,我们永远不会这样做,即使 radius
是可变的。您可以分配不同的值,而 area
和 circumference
将保留它们以前的值,现在这些值是不正确的。
为了正确处理缓存失效,我们需要这样做:
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 规则:
在 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;
}
当然,如果您需要在构造函数之外在内部为字段赋值,您可能需要执行“私有字段,公共 getter”模式,但在需要之前不要使用它。
考虑对简单的成员使用 =>
#Linter 规则:
除了对函数表达式使用 =>
之外,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 时,这是惯用的。
set area(double value) => _area = value;
```dart tag=good
num get x => center.x;
set x(num value) => center = Point(value, center.y);
除非要重定向到命名构造函数或避免隐藏,否则不要使用 this.
#Linter 规则:
JavaScript 需要显式使用 this.
来引用当前正在执行其方法的对象上的成员,但 Dart——与 C++、Java 和 C# 一样——没有这种限制。
只有两种情况下需要使用 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);
// 这将无法解析或编译!
// 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 {
Object? value;
Box(Object? value)
: value = value,
super(value);
}
这看起来令人惊讶,但按您的预期工作。幸运的是,由于初始化形式和超级初始化器,这样的代码相对较少。
尽可能在字段声明时初始化字段
#如果字段不依赖于任何构造函数参数,则可以在其声明时初始化它,也应该这样做。当类具有多个构造函数时,它会减少代码量并避免重复。
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 = '';
}
有些字段不能在其声明处初始化,因为它们需要引用 this
——例如,使用其他字段或调用方法。但是,如果字段被标记为 late
,则初始化程序 可以 访问 this
。
当然,如果字段依赖于构造函数参数,或者由不同的构造函数以不同的方式初始化,则此准则不适用。
构造函数
#以下最佳实践适用于声明类的构造函数。
尽可能使用初始化形式参数
#Linter 规则:
许多字段都是直接从构造函数参数初始化的,例如:
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.
语法称为“初始化形式参数”。您不能总是利用它。有时您希望命名参数的名称与您正在初始化的字段的名称不匹配。但是,当您可以使用初始化形式参数时,您 应该 使用它。
在构造函数初始化列表可以实现时,不要使用 late
#Dart 要求您在读取不可为空的字段之前初始化它们。由于可以在构造函数体内部读取字段,这意味着如果您在主体运行之前没有初始化不可为空的字段,则会收到错误。
您可以通过将字段标记为 late
来消除此错误。如果您在初始化字段之前访问该字段,这会将编译时错误转换为 运行时 错误。在某些情况下您需要这样做,但通常正确的解决方法是在构造函数初始化列表中初始化字段:
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;
}
}
初始化列表使您可以访问构造函数参数,并允许您在读取字段之前初始化它们。因此,如果可以使用初始化列表,则这比将字段设为 late
并丢失一些静态安全性和性能更好。
对空构造函数体使用 ;
代替 {}
#Linter 规则:
在 Dart 中,具有空主体的构造函数可以用分号终止。(实际上,对于 const 构造函数来说,这是必需的。)
class Point {
double x, y;
Point(this.x, this.y);
}
class Point {
double x, y;
Point(this.x, this.y) {}
}
不要使用 new
#Linter 规则:
调用构造函数时, new
关键字是可选的。它的含义并不明确,因为工厂构造函数意味着 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 规则:
在表达式 必须 是常量的上下文中, const
关键字是隐式的,不需要编写,也不应该编写。这些上下文是其中的任何表达式:
- 常量集合字面量。
- 常量构造函数调用
- 元数据注释。
- 常量变量声明的初始化器。
- switch case 表达式——
case
之后到:
之前的部分,而不是 case 的主体。
(此列表中不包含默认值,因为 Dart 的未来版本可能支持非 const 默认值。)
基本上,在任何编写 new
而不是 const
会出错的地方,Dart 都允许您省略 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
子句的捕获
#Linter 规则:
没有 on
限定符的 catch 子句会捕获 try 块中的代码抛出的 任何内容 。 口袋妖怪异常处理 很可能不是您想要的。您的代码是否正确处理了 StackOverflowError 或 OutOfMemoryError ?如果您在该 try 块中错误地向方法传递了错误的参数,您是想让调试器将您指向错误,还是宁愿吞下有用的 ArgumentError ?您是否希望该代码中的任何 assert()
语句实际上消失,因为您正在捕获抛出的 AssertionError ?
答案可能是“否”,在这种情况下,您应该过滤要捕获的类型。在大多数情况下,您应该有一个 on
子句,该子句将您限制为您知道的并正在正确处理的运行时故障类型。
在极少数情况下,您可能希望捕获任何运行时错误。这通常是在框架或低级代码中,这些代码试图隔离任意应用程序代码,以防止其导致问题。即使在这里,通常也最好捕获 Exception ,而不是捕获所有类型。Exception 是所有 运行时 错误的基类,并且不包括指示代码中存在 编程 错误的错误。
不要丢弃没有 on
子句的捕获中的错误
#如果您确实觉得需要捕获代码区域中可能抛出的 所有内容 ,请对捕获的内容执行 某些操作 。记录它,将其显示给用户或重新抛出它,但不要将其静默丢弃。
仅对编程错误抛出实现 Error
的对象
#Error 类是 编程 错误的基类。当抛出该类型或其子接口(如 ArgumentError )的对象时,这意味着代码中存在 错误 。当您的 API 想要向调用者报告它正在被错误使用时,抛出 Error 会清楚地发出该信号。
相反,如果异常是某种运行时故障,并不表示代码中存在错误,则抛出 Error 会产生误导。相反,请抛出核心 Exception 类或其他一些类型之一。
不要显式捕获 Error
或实现它的类型
#Linter 规则:
这遵循上述内容。由于 Error 表示代码中的错误,它应该展开整个调用栈,停止程序并打印堆栈跟踪,以便您可以找到并修复该错误。
捕获这些类型的错误会破坏该过程并掩盖该错误。不要 添加 错误处理代码来处理此异常,而是返回并修复导致它首先被抛出的代码。
应使用 rethrow
重新抛出捕获的异常
#Linter 规则:
如果您决定重新抛出异常,请首选使用 rethrow
语句,而不是使用 throw
抛出相同的异常对象。 rethrow
保留异常的原始堆栈跟踪。另一方面, throw
将堆栈跟踪重置为最后抛出的位置。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
异步
#Dart 有几个语言特性来支持异步编程。以下最佳实践适用于异步编码。
首选 async/await 而不是使用原始 future
#异步代码非常难以阅读和调试,即使使用像 future 这样的不错的抽象也是如此。 async
/ await
语法提高了可读性,并允许您在异步代码中使用所有 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;
}
}
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
然后throw
比return Future.error(...)
更短。您正在返回值,并且希望它隐式地包装在 future 中。
async
比Future.value(...)
更短。
Future<void> usesAwait(Future<String> later) async {
print(await later);
}
Future<void> asyncError() async {
throw 'Error!';
}
Future<String> asyncValue() async => 'value';
考虑使用高阶方法来转换流
#这与上面关于可迭代对象的建议类似。流支持许多相同的方法,并且还正确处理诸如传输错误、关闭等方面的事情。
避免直接使用 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');
}
在区分类型参数可以是 Object
的 FutureOr<T>
时,应测试 Future<T>
#在您可以对 FutureOr<T>
执行任何有用的操作之前,您通常需要进行 is
检查以查看您是否有 Future<T>
或裸 T
。如果类型参数是一些特定类型,例如 FutureOr<int>
,那么您使用哪个测试并不重要, is int
或 is Future<int>
。两者都有效,因为这两个类型是不相交的。
但是,如果值类型是 Object
或可以可能用 Object
实例化的类型参数,则这两个分支会重叠。 Future<Object>
本身实现了 Object
,因此 is Object
或 is 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>
,它会错误地将其视为裸的同步值。
除非另有说明,否则本网站上的文档反映的是 Dart 3.6.0。页面最后更新于 2025-02-05。 查看源代码 或 报告问题.