Effective Dart:使用
您可以在 Dart 代码体中每天使用这些指南。您的库的用户可能无法分辨出您是否已将此处提出的想法内化,但您的库的维护者肯定会注意到。
库
#这些指南可帮助您以一致且可维护的方式将程序分解成多个文件。为了使这些指南简洁明了,它们使用“导入”来涵盖 `import` 和 `export` 指令。这些指南对两者都适用。
请在 `part of` 指令中使用字符串
#代码风格规则:use_string_in_part_of_directives
许多 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` 目录内的库
#代码风格规则:implementation_imports
`lib` 下的 `src` 目录指定用于包含包自身实现的私有库。包维护者对包进行版本控制的方式会考虑此约定。他们可以自由地对 `src` 下的代码进行重大更改,而不会对包造成重大更改。
这意味着,如果您导入其他包的私有库,则该包的次要(理论上不破坏)的点版本发布可能会破坏您的代码。
请勿允许导入路径到达或超出 `lib`
#代码风格规则: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
import 'package:my_package/api.dart';
import '../lib/api.dart';
Dart认为这些是两个完全无关的库的导入。为了避免混淆 Dart 和您自己,请遵循以下两条规则
- 在导入路径中不要使用
/lib/
。 - 不要使用
../
退出lib
目录。
相反,当您需要进入包的lib
目录(即使是从同一个包的test
目录或任何其他顶级目录)时,请使用package:
导入。
import 'package:my_package/api.dart';
包永远不应该超出其lib
目录并导入包中其他位置的库。
优先使用相对导入路径
#代码风格规则:prefer_relative_imports
每当前面的规则不起作用时,请遵循此规则。当导入不跨越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'; // Don't reach into 'lib'.
import 'test_utils.dart'; // Relative within 'test' is fine.
空值
#不要显式地将变量初始化为null
#代码风格规则:avoid_init_to_null
如果变量具有非空类型,则如果您尝试在它被明确初始化之前使用它,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
的显式默认值
#代码风格规则:avoid_init_to_null
如果您使可空参数可选但没有为其提供默认值,则语言会隐式使用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
检查。
// 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
是一个可行的表达式,但由于以下几个原因不应使用它
它没有表明代码与
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 中组合字符串时需要注意的一些最佳实践。
请使用相邻字符串连接字符串字面量
#代码风格规则: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.');
优先使用插值来组合字符串和值
#代码风格规则:prefer_interpolation_to_compose_strings
如果您来自其他语言,您习惯于使用长串+
来从字面量和其他值构建字符串。这在 Dart 中确实有效,但几乎总是使用插值更简洁、更短
'Hello, $name! You are ${year - birth} years old.';
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
请注意,此准则适用于组合多个字面量和值。在仅将单个对象转换为字符串时,使用.toString()
是可以的。
在不需要时,请避免在插值中使用花括号
#代码风格规则: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 支持四种集合类型:列表、映射、队列和集合。以下最佳实践适用于集合。
请尽可能使用集合字面量
#代码风格规则:prefer_collection_literals
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
来查看集合是否为空
#代码风格规则:prefer_is_empty,prefer_is_not_empty
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()
#代码风格规则: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()
#给定一个 Iterable,有两种明显的方法可以生成一个包含相同元素的新 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);
但是,如果您的目标只是复制 Iterable 并保留其原始类型,或者您不关心类型,则使用toList()
。
使用whereType()
按类型筛选集合
#代码风格规则:prefer_iterable_whereType
假设您有一个包含各种对象的列表,并且您想从中获取所有整数。您可以像这样使用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) {
// 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()
积极转换的示例:
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 中,即使是函数也是对象。以下是一些涉及函数的最佳实践。
请使用函数声明将函数绑定到名称
#代码风格规则:prefer_function_declarations_over_variables
现代语言已经意识到局部嵌套函数和闭包的实用性。在另一个函数内部定义一个函数是很常见的。在许多情况下,此函数立即用作回调,并且不需要名称。函数表达式非常适合这种情况。
但是,如果您确实需要为其命名,请使用函数声明语句,而不是将 lambda 绑定到变量。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
当可以使用断言时,请勿创建 lambda 表达式
#代码风格规则:unnecessary_lambdas
当您引用函数、方法或命名构造函数而不带括号时,Dart 会创建一个撕裂。这是一个闭包,它接受与函数相同的参数,并在您调用它时调用底层函数。如果您的代码需要一个闭包,它使用与闭包接受的参数相同的参数调用命名函数,请不要将调用包装在 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));
变量
#以下最佳实践描述了如何在 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 中
#代码风格规则: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;
}
当然,如果您需要在构造函数之外在内部为该字段赋值,则可能需要使用“私有字段,公共 getter”模式,但在您需要之前不要使用它。
**考虑**对简单的成员使用 =>
#代码风格规则: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.
,除非重定向到命名构造函数或避免隐藏
#代码风格规则:unnecessary_this
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);
// 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);
}
这看起来令人惊讶,但按您期望的方式工作。幸运的是,由于初始化形式参数和超级初始化程序,此类代码相对较少。
请尽可能在字段声明时进行初始化
#如果字段不依赖于任何构造函数参数,则可以在其声明处初始化它,也应该这样做。它需要更少的代码,并且当类具有多个构造函数时避免重复。
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
。
当然,如果字段依赖于构造函数参数,或由不同的构造函数以不同的方式初始化,则此准则不适用。
构造函数
#以下最佳实践适用于为类声明构造函数。
请尽可能使用初始化形式参数
#代码风格规则:prefer_initializing_formals
许多字段直接从构造函数参数初始化,例如
class Point {
double x, y;
Point(double x, double y)
: x = x,
y = y;
}
我们必须在这里键入 4 次 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
更好,因为后者会失去一些静态安全性和性能。
对于空构造函数体,使用;
而不是{}
#代码风格规则:empty_constructor_bodies
在 Dart 中,空构造函数体可以用分号结束。(事实上,对于 const 构造函数,这是必需的。)
class Point {
double x, y;
Point(this.x, this.y);
}
class Point {
double x, y;
Point(this.x, this.y) {}
}
不要使用new
#代码风格规则:unnecessary_new
调用构造函数时,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
#代码风格规则:unnecessary_const
在表达式必须为常量的情况下,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
子句的 catch
#代码风格规则:avoid_catches_without_on_clauses
没有on
限定符的 catch 子句会捕获try块中代码抛出的任何内容。精灵宝可梦异常处理很可能不是你想要的。你的代码是否正确地处理了StackOverflowError或OutOfMemoryError?如果在该 try 块中错误地向方法传递了错误的参数,你希望调试器将你指向错误,还是宁愿那个有帮助的ArgumentError被吞掉?你希望该代码中的任何assert()
语句都消失,因为你正在捕获抛出的AssertionError吗?
答案可能是“否”,在这种情况下,你应该过滤你捕获的类型。在大多数情况下,你应该有一个on
子句,将你限制在你意识到的并正确处理的运行时故障类型。
在极少数情况下,你可能希望捕获任何运行时错误。这通常是在框架或低级代码中,这些代码试图防止任意应用程序代码导致问题。即使在这里,通常也最好捕获Exception而不是捕获所有类型。Exception 是所有运行时错误的基类,并且排除了指示代码中程序性错误的错误。
不要丢弃来自不带on
子句的 catch 的错误
#如果你确实觉得需要捕获代码区域中可能抛出的所有内容,请对捕获的内容做点什么。记录它,显示给用户或重新抛出它,但不要静默地丢弃它。
仅对程序性错误抛出实现Error
的对象
#Error类是程序性错误的基类。当抛出该类型或其子接口(如ArgumentError)的对象时,表示代码中存在错误。当你的 API 想要向调用方报告它被错误使用时,抛出一个 Error 会清楚地发出此信号。
相反,如果异常是某种不表示代码中错误的运行时故障,则抛出 Error 会产生误导。相反,抛出核心 Exception 类之一或其他一些类型。
不要显式捕获Error
或实现它的类型
#代码风格规则:avoid_catching_errors
这源于上述内容。由于 Error 指示代码中存在错误,因此它应该展开整个调用栈,停止程序并打印堆栈跟踪,以便你找到并修复错误。
捕获这些类型的错误会破坏该过程并掩盖错误。与其在事后添加错误处理代码来处理此异常,不如返回并修复导致它被抛出的代码本身。
使用rethrow
重新抛出捕获的异常
#代码风格规则:use_rethrow_when_possible
如果你决定重新抛出异常,建议使用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);
}
异步
#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.5.3。页面上次更新于 2024-10-16。 查看源代码 或 报告问题.