高效 Dart 语言指南:用法示例
每天在你写的 Dart 代码中都会应用到这些准则。库的使用者可能不需要知道你在其中的一些想法,但是维护者肯定是需要的。
库
这些准则可以帮助你在多个文件编写程序的情况下保证一致性和可维护性。为了让准则简洁,这里使用“import”来同时代表 import
和 export
。准则同时适用于这两者。
part of
directives
DO use strings in Linter rule: use_string_in_part_of_directives
part of
中使用字符串
要 在 很多 Dart 开发者会避免直接使用 part
。他们发现当库仅有一个文件的时候很容易读懂代码。如果你确实要使用 part
将库的一部分拆分为另一个文件,则 Dart 要求另一个文件指示它所属库的路径。
由于遗留原因,Dart 允许 part of
指令使用它所属的库的 名称。这使得工具很难直接查找到这个文件对应主库文件,使得库和文件之间的关系模糊不清。
推荐的现代语法是使用 URI 字符串直接指向库文件。首选的现代语法是使用直接指向库文件的URI字符串,URI 的使用和其他指令中一样。如果你有一些库,my_library.dart
,其中包含:
library my_library;
part 'some/other/file.dart';
从库中拆分的文件应该如下所示:
part of '../../my_library.dart';
而不是:
part of my_library;
src
目录下的库
不要 导入 package 中 Linter rule: implementation_imports
lib
下的 src
目录 被指定 为 package 自己实现的私有库。基于包维护者对版本的考虑,package 使用了这种约定。在不破坏 package 的情况下,维护者可以自由地对 src
目录下的代码进行修改。
这意味着,你如果导入了其中的私有库,按理论来讲,一个不破坏 package 的次版本就会影响到你的代码。
lib
DON’T allow an import path to reach into or out of Linter rule: avoid_relative_lib_imports
A package:
import lets you access
a library inside a package’s lib
directory
without having to worry about where the package is stored on your computer.
For this to work, you cannot have imports that require the lib
to be in some location on disk relative to other files.
In other words, a relative import path in a file inside lib
can’t reach out and access a file outside of the lib
directory,
and a library outside of lib
can’t use a relative path
to reach into the lib
directory.
Doing either leads to confusing errors and broken programs.
For example, say your directory structure looks like this:
my_package
└─ lib
└─ api.dart
test
└─ api_test.dart
And say api_test.dart
imports api.dart
in two ways:
import 'package:my_package/api.dart';
import '../lib/api.dart';
Dart thinks those are imports of two completely unrelated libraries. To avoid confusing Dart and yourself, follow these two rules:
- Don’t use
/lib/
in import paths. - Don’t use
../
to escape thelib
directory.
Instead, when you need to reach into a package’s lib
directory
(even from the same package’s test
directory
or any other top-level directory),
use a package:
import.
import 'package:my_package/api.dart';
A package should never reach out of its lib
directory and
import libraries from other places in the package.
PREFER relative import paths
Linter rule: prefer_relative_imports
比如,下面是你的 package 目录结构:
my_package
└─ lib
├─ src
│ └─ stuff.dart
│ └─ utils.dart
└─ api.dart
test
│─ api_test.dart
└─ test_utils.dart
Here is how the various libraries should import each other:
如果 api.dart
想导入 utils.dart
,应该这样使用:
import 'src/stuff.dart';
import 'src/utils.dart';
lib/src/utils.dart:
import '../api.dart';
import 'stuff.dart';
test/api_test.dart:
import 'package:my_package/api.dart'; // Don't reach into 'lib'.
import 'test_utils.dart'; // Relative within 'test' is fine.
Null
null
DON’T explicitly initialize variables to Linter rule: avoid_init_to_null
If a variable has a non-nullable type, Dart reports a compile error if you try
to use it before it has been definitely initialized. If the variable is
nullable, then it is implicitly initialized to null
for you. There’s no
concept of “uninitialized memory” in Dart and no need to explicitly initialize a
variable to null
to be “safe”.
Item? bestDeal(List<Item> cart) {
Item? bestItem;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}
Item? bestDeal(List<Item> cart) {
Item? bestItem = null;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}
null
DON’T use an explicit default value of Linter rule: avoid_init_to_null
If you make a nullable parameter optional but don’t give it a default value, the
language implicitly uses null
as the default, so there’s no need to write it.
void error([String? message]) {
stderr.write(message ?? '\n');
}
void error([String? message = null]) {
stderr.write(message ?? '\n');
}
true
or false
in equality operations
DON’T use Using the equality operator to evaluate a non-nullable boolean expression
against a boolean literal is redundant.
It’s always simpler to eliminate the equality operator,
and use the unary negation operator !
if necessary:
if (nonNullableBool) { ... }
if (!nonNullableBool) { ... }
if (nonNullableBool == true) { ... }
if (nonNullableBool == false) { ... }
To evaluate a boolean expression that is nullable, you should use ??
or an explicit != null
check.
// If you want null to result in false:
if (nullableBool ?? false) { ... }
// If you want null to result in false
// and you want the variable to type promote:
if (nullableBool != null && nullableBool) { ... }
// Static error if null:
if (nullableBool) { ... }
// If you want null to be false:
if (nullableBool == true) { ... }
nullableBool == true
is a viable expression,
but shouldn’t be used for several reasons:
-
It doesn’t indicate the code has anything to do with
null
. -
Because it’s not evidently
null
related, it can easily be mistaken for the non-nullable case, where the equality operator is redundant and can be removed. That’s only true when the boolean expression on the left has no chance of producing null, but not when it can. -
The boolean logic is confusing. If
nullableBool
is null, thennullableBool == true
means the condition evaluates tofalse
.
The ??
operator makes it clear that something to do with null is happening,
so it won’t be mistaken for a redundant operation.
The logic is much clearer too;
the result of the expression being null
is the same as the boolean literal.
Using a null-aware operator such as ??
on a variable inside a condition
doesn’t promote the variable to a non-nullable type.
If you want the variable to be promoted inside the body of the if
statement,
it’s better to use an explicit != null
check instead of ??
.
late
variables if you need to check whether they are initialized
AVOID Dart offers no way to tell if a late
variable
has been initialized or assigned to.
If you access it, it either immediately runs the initializer
(if it has one) or throws an exception.
Sometimes you have some state that’s lazily initialized
where late
might be a good fit,
but you also need to be able to tell if the initialization has happened yet.
Although you could detect initialization by storing the state in a late
variable
and having a separate boolean field
that tracks whether the variable has been set,
that’s redundant because Dart internally
maintains the initialized status of the late
variable.
Instead, it’s usually clearer to make the variable non-late
and nullable.
Then you can see if the variable has been initialized
by checking for null
.
Of course, if null
is a valid initialized value for the variable,
then it probably does make sense to have a separate boolean field.
CONSIDER assigning a nullable field to a local variable to enable type promotion
Checking that a nullable variable is not equal to null
promotes the variable
to a non-nullable type. That lets you access members on the variable and pass it
to functions expecting a non-nullable type.
Type promotion is only supported, however, for local variables, parameters, and private final fields. Values that are open to manipulation can’t be type promoted.
Declaring members private and final, as we generally recommend, is often enough to bypass these limitations. But, that’s not always an option. One pattern to work around this is to assign the field’s value to a local variable. Null checks on that variable will promote, so you can safely treat it as non-nullable.
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
final response = this.response;
if (response != null) {
return 'Could not complete upload to ${response.url} '
'(error code ${response.errorCode}): ${response.reason}.';
}
return 'Could not upload (no response).';
}
}
Assigning to a local variable can be cleaner and safer than using !
every
time you need to treat the value as non-null:
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
if (response != null) {
return 'Could not complete upload to ${response!.url} '
'(error code ${response!.errorCode}): ${response!.reason}.';
}
return 'Could not upload (no response).';
}
}
Be careful when using a local variable. If you need to write back to the field,
make sure that you don’t write back to the local variable instead. (Making the
local variable final
can prevent such mistakes.) Also, if the field might
change while the local is still in scope, then the local might have a stale
value. Sometimes it’s best to simply use !
on the field.
字符串
下面是一些需要记住的,关于在 Dart 中使用字符串的最佳实践。
要 使用相邻字符串的方式连接字面量字符串
Linter rule: prefer_adjacent_string_concatenation
如果你有两个字面量字符串(不是变量,是放在引号中的字符串),你不需要使用 +
来连接它们。应该像 C 和 C++ 一样,只需要将它们挨着在一起就可以了。这种方式非常适合不能放到一行的长字符串的创建。
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
'parts are overrun by martians. Unclear which are which.');
推荐 使用插值的形式来组合字符串和值
Linter rule: prefer_interpolation_to_compose_strings
如果你之前使用过其他语言,你一定习惯使用大量 +
将字面量字符串以及字符串变量链接构建字符串。这种方式在 Dart 中同样有效,但是通常情况下使用插值会更清晰简短:
'Hello, $name! You are ${year - birth} years old.';
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
Note that this guideline applies to combining multiple literals and values.
It’s fine to use .toString()
when converting only a single object to a string.
避免 在字符串插值中使用不必要的大括号
Linter rule: unnecessary_brace_in_string_interps
如果要插入是一个简单的标识符,并且后面没有紧跟随在其他字母文本,则应省略 {}
。
var greeting = 'Hi, $name! I love your ${decade}s costume.';
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';
集合
Dart 集合中原生支持了四种类型:list, map, queue,和 set。下面是应用于集合的最佳实践。
要 尽可能的使用集合字面量
Linter rule: prefer_collection_literals
Dart 有三种核心集合类型。List、Map 和 Set,这些类和大多数类一样,都有未命名的构造函数,但由于这些集合使用频率很高,Dart 有更好的内置语法来创建它们:
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
var addresses = Map<String, Address>();
var counts = Set<int>();
Note that this guideline doesn’t apply to the named constructors for those
classes. List.from()
, Map.fromIterable()
, and friends all have their uses.
(The List class also has an unnamed constructor, but it is prohibited in null
safe Dart.)
Collection literals are particularly powerful in Dart
because they give you access to the spread operator
for including the contents of other collections,
and if
and for
for performing control flow while
building the contents:
var arguments = [
...options,
command,
...?modeFlags,
for (var path in filePaths)
if (path.endsWith('.dart')) path.replaceAll('.dart', '.js')
];
var arguments = <String>[];
arguments.addAll(options);
arguments.add(command);
if (modeFlags != null) arguments.addAll(modeFlags);
arguments.addAll(filePaths
.where((path) => path.endsWith('.dart'))
.map((path) => path.replaceAll('.dart', '.js')));
注意,对于集合类的 命名 构造函数则不适用上面的规则。
List.from()
、 Map.fromIterable()
都有其使用场景。如果需要一个固定长度的结合,使用 List()
来创建一个固定长度的 list 也是合理的。
.length
来判断一个集合是否为空
不要 使用 Linter rules: prefer_is_empty, prefer_is_not_empty
Iterable 合约并不要求集合知道其长度,也没要求在遍历的时候其长度不能改变。通过调用 .length
来判断集合是否包含内容是非常低效的。
相反,Dart 提供了更加高效率和易用的 getter 函数:.isEmpty
和.isNotEmpty
。使用这些函数并不需要对结果再次取非。
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');
Iterable.forEach()
中使用字面量函数
避免 在 Linter rule: avoid_function_literals_in_foreach_calls
forEach()
函数在 JavaScript 中被广泛使用,这因为内置的 for-in
循环通常不能达到你想要的效果。在Dart中,如果要对序列进行迭代,惯用的方式是使用循环。
for (final person in people) {
...
}
people.forEach((person) {
...
});
例外情况是,如果要执行的操作是调用一些已存在的并且将每个元素作为参数的函数,在这种情况下,forEach()
是很方便的。
people.forEach(print);
您可以调用 Map.forEach()
。Map 是不可迭代的,所以该准则对它无效。
List.from()
除非想修改结果的类型
不要 使用 给定一个可迭代的对象,有两种常见方式来生成一个包含相同元素的 list:
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
明显的区别是前一个更短。更重要的区别在于第一个保留了原始对象的类型参数:
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<int>":
print(iterable.toList().runtimeType);
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);
如果你想要改变类型,那么可以调用 List.from()
:
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);
但是如果你的目的只是复制可迭代对象并且保留元素原始类型,或者并不在乎类型,那么请使用 toList()
。
whereType()
按类型过滤集合
要 使用 Linter rule: prefer_iterable_whereType
假设你有一个 list 里面包含了多种类型的对象,但是你指向从它里面获取整型类型的数据。那么你可以像下面这样使用 where()
:
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);
这个很罗嗦,但是更糟糕的是,它返回的可迭代对象类型可能并不是你想要的。在上面的例子中,虽然你想得到一个 Iterable<int>
,然而它返回了一个 Iterable<Object>
,这是因为,这是你过滤后得到的类型。
有时候你会看到通过添加 cast()
来“修正”上面的错误:
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();
代码冗长,并导致创建了两个包装器,获取元素对象要间接通过两层,并进行两次多余的运行时检查。幸运的是,对于这个用例,核心库提供了 whereType()
方法:
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();
使用 whereType()
简洁,生成所需的 Iterable(可迭代)类型,并且没有不必要的层级包装。
cast()
,如果有更合适的方法
不要 使用 通常,当处理可迭代对象或 stream 时,你可以对其执行多次转换。最后,生成所希望的具有特定类型参数的对象。尝试查看是否有已有的转换方法来改变类型,而不是去掉用 cast()
。而不是调用 cast()
,看看是否有一个现有的转换可以改变类型。
如果你已经使用了 toList()
,那么请使用 List<T>.from()
替换,这里的 T
是你想要的返回值的类型。
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();
如果你正在调用 map()
,给它一个显式的类型参数,这样它就能产生一个所需类型的可迭代对象。类型推断通常根据传递给 map()
的函数选择出正确的类型,但有的时候需要明确指明。
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();
cast()
避免 使用 这是对先前规则的一个宽松的定义。有些时候,并没有合适的方式来修改对象类型,即便如此,也应该尽可能的避免使用 cast()
来“改变”集合中元素的类型。
推荐使用下面的方式来替代:
-
用恰当的类型创建集合。 修改集合被首次创建时的代码,为集合提供有一个恰当的类型。
-
在访问元素时进行 cast 操作。 如果要立即对集合进行迭代,在迭代内部 cast 每个元素。
-
逼不得已进行 cast,请使用
List.from()
。 如果最终你会使用到集合中的大部分元素,并且不需要对象还原到原始的对象类型,使用List.from()
来转换它。cast()
方法返回一个惰性集合 (lazy collection) ,每个操作都会对元素进行检查。如果只对少数元素执行少量操作,那么这种惰性方式就非常合适。但在许多情况下,惰性验证和包裹 (wrapping) 所产生的开销已经超过了它们所带来的好处。
下面是 用恰当的类型创建集合 的示例:
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}
下面是 在访问元素时进行 cast 操作 的示例:
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (final n in objects) {
if ((n as int).isEven) print(n);
}
}
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (final n in objects.cast<int>()) {
if (n.isEven) print(n);
}
}
下面是 使用 List.from()
进行 cast 操作 的示例:
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = objects.cast<int>();
ints.sort();
return ints[ints.length ~/ 2];
}
当然,这些替代方案并不总能解决问题,显然,这时候就应该选择 cast()
方式了。但是考虑到这种方式的风险和缺点——如果使用不当,可能会导致执行缓慢和运行失败。
函数
在 Dart 中,就连函数也是对象。以下是一些涉及函数的最佳实践。
要 使用函数声明的方式为函数绑定名称
Linter rule: prefer_function_declarations_over_variables
现代语言已经意识到本地嵌套函数和闭包的益处。在一个函数中定义另一个函数非常常见。在许多情况下,这些函数被立即执行并返回结果,而且不需要名字。这种情况下非常适合使用函数表达式来实现。
但是,如果你确实需要给方法一个名字,请使用方法定义而不是把 lambda 赋值给一个变量。
void main() {
void localFunction() {
...
}
}
void main() {
var localFunction = () {
...
};
}
不要 使用 lambda 表达式来替代 tear-off
Linter rule: unnecessary_lambdas
如果你引用了一个函数、方法或命名构造,但省略了括号,Dart 会尝试 tear-off——在调用时使用同样的参数对对应的方法创建闭包。如果你需要的仅仅是一个引用,请不要利用 lambda 手动包装。
如果你有一个方法,这个方法调用了参数相同的另一个方法。那么,你不需要人为将这个方法包装到一个 lambda 表达式中。
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach(print);
// Method:
charCodes.forEach(buffer.write);
// Named constructor:
var strings = charCodes.map(String.fromCharCode);
// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach((code) {
print(code);
});
// Method:
charCodes.forEach((code) {
buffer.write(code);
});
// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));
// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));
变量
The following best practices describe how to best use variables in Dart.
var
and final
on local variables
DO follow a consistent rule for Most local variables shouldn’t have type annotations and should be declared
using just var
or final
. There are two rules in wide use for when to use one
or the other:
-
Use
final
for local variables that are not reassigned andvar
for those that are. -
Use
var
for all local variables, even ones that aren’t reassigned. Never usefinal
for locals. (Usingfinal
for fields and top-level variables is still encouraged, of course.)
Either rule is acceptable, but pick one and apply it consistently throughout
your code. That way when a reader sees var
, they know whether it means that
the variable is assigned later in the function.
避免 保存可计算的结果
在设计类的时候,你常常希望暴露底层状态的多个表现属性。常常你会发现在类的构造函数中计算这些属性,然后保存起来:
class Circle {
double radius;
double area;
double circumference;
Circle(double radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
上面的代码有两个不妥之处。首先,这样浪费了内存。严格来说面积和周长是缓存数据。他们保存的结果可以通过已知的数据计算出来。他们减少了 CPU 消耗却增加了内存消耗。我们还没有权衡,到底存不存在性能问题?
更糟糕的是,代码是错误的。问题在于缓存是无效的 —— 你如何知道缓存何时会过期并且需要重新计算?即便半径是可变的,在这里我们也永远不会这样做。你可以赋一个不同的值,但面积和周长还是以前的值,现在的值是不正确的。
为了正确处理缓存失效,我们需要这样做:
class Circle {
double _radius;
double get radius => _radius;
set radius(double value) {
_radius = value;
_recalculate();
}
double _area = 0.0;
double get area => _area;
double _circumference = 0.0;
double get circumference => _circumference;
Circle(this._radius) {
_recalculate();
}
void _recalculate() {
_area = pi * _radius * _radius;
_circumference = pi * 2.0 * _radius;
}
}
这需要编写、维护、调试以及阅读更多的代码。如果你一开始这样写代码:
class Circle {
double radius;
Circle(this.radius);
double get area => pi * radius * radius;
double get circumference => pi * 2.0 * radius;
}
上面的代码更加简洁、使用更少的内存、减少出错的可能性。它尽可能少的保存了表示圆所需要的数据。这里没有字段需要同步,因为这里只有一个有效数据源。
在某些情况下,当计算结果比较费时的时候可能需要缓存,但是只应该在你只有你有这样的性能问题的时候再去处理,处理时要仔细,并留下挂关于优化的注释。
成员
在 Dart 中,对象成员可以是函数(方法)或数据(实例变量)。下面是关于对象成员的最佳实践。
不要 为字段创建不必要的 getter 和 setter 方法
Linter rule: unnecessary_getters_setters
在 Java 和 C# 中,通常情况下会将所有的字段隐藏到 getter 和 setter 方法中(在 C# 中被称为属性),即使实现中仅仅是指向这些字段。在这种方式下,即使你在这些成员上做多少的事情,你也不需要直接访问它们。这是因为,在 Java 中,调用 getter 方法和直接访问字段是不同的。在 C# 中,访问属性与访问字段不是二进制兼容的。
Dart 不存在这个限制。字段和 getter/setter 是完全无法区分的。你可以在类中公开一个字段,然后将其包装在 getter 和 setter 中,而不会影响任何使用该字段的代码。
class Box {
Object? contents;
}
class Box {
Object? _contents;
Object? get contents => _contents;
set contents(Object? value) {
_contents = value;
}
}
final
关键字来创建只读属性
推荐 使用 如果一个变量对于外部代码来说只能读取不能修改,最简单的做法就是使用 final
关键字来标记这个变量。
class Box {
final contents = [];
}
class Box {
Object? _contents;
Object? get contents => _contents;
}
当然,如果你需要构造一个内部可以赋值,外部可以访问的字段,你可以需要这种“私有成员变量,公开访问函数”的模式,但是,如非必要,请不要使用这种模式。
=>
考虑 对简单成员使用 Linter rule: prefer_expression_function_bodies
除了使用 =>
可以用作函数表达式以外,
Dart 还允许使用它来定义成员。这种风格非常适合,仅进行计算并返回结果的简单成员。
double get area => (right - left) * (bottom - top);
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
编写代码的人似乎很喜欢 =>
语法,但是它很容易被滥用,最后导致代码不容易被阅读。如果你有很多行声明或包含深层的嵌套表达式(级联和条件运算符就是常见的罪魁祸首),你以及其他人有谁会愿意读这样的代码!你应该换做使用代码块和一些语句来实现。
Treasure? openChest(Chest chest, Point where) {
if (_opened.containsKey(chest)) return null;
var treasure = Treasure(where);
treasure.addAll(chest.contents);
_opened[chest] = treasure;
return treasure;
}
Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
? null
: _opened[chest] = (Treasure(where)..addAll(chest.contents));
您还可以对不返回值的成员使用 =>
。这里有个惯例,就是当 setter 和 getter 都比较简单的时候使用 =>
。
num get x => center.x;
set x(num value) => center = Point(value, center.y);
this.
,在重定向命名函数和避免冲突的情况下除外
不要 使用 Linter rule: unnecessary_this
JavaScript 需要使用 this.
来引用对象的成员变量,但是 Dart—和 C++, Java, 以及C#—没有这种限制。
只有当局部变量和成员变量名字一样的时候,你才需要使用 this.
来访问成员变量。只有两种情况需要使用 this.
,其中一种情况是要访问的局部变量和成员变量命名一样的时候:
class Box {
Object? value;
void clear() {
this.update(null);
}
void update(Object? value) {
this.value = value;
}
}
class Box {
Object? value;
void clear() {
update(null);
}
void update(Object? value) {
this.value = value;
}
}
另一种使用 this.
的情况是在重定向到一个命名函数的时候:
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// This won't parse or compile!
// ShadeOfGray.alsoBlack() : black();
}
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// But now it will!
ShadeOfGray.alsoBlack() : this.black();
}
注意,构造函数初始化列表中的字段有永远不会与构造函数参数列表参数产生冲突。
class Box extends BaseBox {
Object? value;
Box(Object? value)
: value = value,
super(value);
}
这看起来很令人惊讶,但是实际结果是你想要的。幸运的是,由于初始化规则的特殊性,上面的代码很少见到。
要 尽可能的在定义变量的时候初始化变量值
If a field doesn’t depend on any constructor parameters, it can and should be initialized at its declaration. It takes less code and avoids duplication when the class has multiple constructors.
class ProfileMark {
final String name;
final DateTime start;
ProfileMark(this.name) : start = DateTime.now();
ProfileMark.unnamed()
: name = '',
start = DateTime.now();
}
class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}
Some fields can’t be initialized at their declarations because they need to reference
this
—to use other fields or call methods, for example. However, if the
field is marked late
, then the initializer can access this
.
当然,对于变量取值依赖构造函数参数的情况以及不同的构造函数取值也不一样的情况,则不适合本条规则。
构造函数
下面对于类的构造函数的最佳实践。
要 尽可能的使用初始化形式
Linter rule: prefer_initializing_formals
许多字段直接使用构造函数参数来初始化,如:
class Point {
double x, y;
Point(double x, double y)
: x = x,
y = y;
}
为了初始化一个字段,我们需要反复写下 x
四次。使用下面的方式会更好:
class Point {
double x, y;
Point(this.x, this.y);
}
This this.
syntax before a constructor parameter is called an “initializing
formal”. You can’t always take advantage of it. Sometimes you want to have a
named parameter whose name doesn’t match the name of the field you are
initializing. But when you can use initializing formals, you should.
late
when a constructor initializer list will do
DON’T use Dart 要求你为非空变量在它们被访问前就初始化好内容。如果你没有初始化,那么在构造函数运行时就会直接报错。
如果构造函数参数使用 this.
的方式来初始化字段,这时参数的类型被认为和字段类型相同。
class Point {
double x, y;
Point.polar(double theta, double radius)
: x = cos(theta) * radius,
y = sin(theta) * radius;
}
class Point {
late double x, y;
Point.polar(double theta, double radius) {
x = cos(theta) * radius;
y = sin(theta) * radius;
}
}
The initializer list gives you access to constructor parameters and lets you
initialize fields before they can be read. So, if it’s possible to use an initializer list,
that’s better than making the field late
and losing some static safety and
performance.
;
来替代空的构造函数体 {}
要 用 Linter rule: empty_constructor_bodies
在 Dart 中,没有具体函数体的构造函数可以使用分号结尾。(事实上,这是不可变构造函数的要求。)
class Point {
double x, y;
Point(this.x, this.y);
}
class Point {
double x, y;
Point(this.x, this.y) {}
}
new
不要 使用 Linter rule: unnecessary_new
Dart 2 new
关键字成为可选项。即使在Dart 1中,其含义也从未明确过,因为在工厂构造函数中,调用 new
可能并不意味着一定会返回一个新对象。
Dart 语言仍然允许使用 new
关键字,但请考虑在你的代码中弃用和避免使用 new
。
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(
child: Text('Increment'),
),
Text('Click!'),
],
);
}
Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(
child: new Text('Increment'),
),
new Text('Click!'),
],
);
}
const
不要 冗余地使用 Linter rule: unnecessary_const
在表达式一定是常量的上下文中,const
关键字是隐式的,不需要写,也不应该。这里包括:
-
一个字面量常量集合。
-
调用一个常量构造函数。
-
元数据注解。
-
一个常量声明的初始化方法。
-
switch case 表达式——
case
和:
中间的部分,不是 case 执行体。
(默认值并不包含在这个列表中,因为在 Dart 将来的版本中可能会在支持非常量的默认值。)
基本上,任何地方用 new
替代 const
的写法都是错的,因为 Dart 2 中允许省略 const
。
const primaryColors = [
Color('red', [255, 0, 0]),
Color('green', [0, 255, 0]),
Color('blue', [0, 0, 255]),
];
const primaryColors = const [
const Color('red', const [255, 0, 0]),
const Color('green', const [0, 255, 0]),
const Color('blue', const [0, 0, 255]),
];
错误处理
Dart 使用异常来表示程序执行错误。下面是关于如何捕获和抛出异常的最佳实践。
on
语句的 catch
避免 使用没有 Linter rule: avoid_catches_without_on_clauses
没有 on
限定的 catch 语句会捕获 try 代码块中抛出的任何异常。
Pokémon exception handling 可能并不是你想要的。你的代码是否正确的处理 StackOverflowError 或者 OutOfMemoryError 异常?如果你使用错误的参数调用函数,你是期望调试器定位出你的错误使用情况还是,把这个有用的 ArgumentError 给吞噬了?由于你捕获了 AssertionError 异常,导致所有 try 块内的 assert()
语句都失效了,这是你需要的结果吗?
答案和可能是 “no”,在这种情况下,您应该过滤掉捕获的类型。在大多数情况下,您应该有一个 on
子句,这样它能够捕获程序在运行时你所关注的限定类型的异常并进行恰当处理。
In rare cases, you may wish to catch any runtime error. This is usually in framework or low-level code that tries to insulate arbitrary application code from causing problems. Even here, it is usually better to catch Exception than to catch all types. Exception is the base class for all runtime errors and excludes errors that indicate programmatic bugs in the code.
on
语句捕获的异常
不要 丢弃没有使用 如果你真的期望捕获一段代码内的 所有 异常,请在捕获异常的地方做些事情。记录下来并显示给用户,或者重新抛出 (rethrow) 异常信息,记得不要默默的丢弃该异常信息。
Error
的异常
要 只在代表编程错误的情况下才抛出实现了 Error 类是所有 编码 错误的基类。当一个该类型或者其子类型,例如 ArgumentError 对象被抛出了,这意味着是你代码中的一个 bug。当你的 API 想要告诉调用者使用错误的时候可以抛出一个 Error 来表明你的意图。
同样的,如果一个异常表示为运行时异常而不是代码 bug,则抛出 Error 则会误导调用者。应该抛出核心定义的 Exception 类或者其他类型。
Error
或者其子类
不要 显示的捕获 Linter rule: avoid_catching_errors
本条衔接上一条的内容。既然 Error 表示代码中的 bug,应该展开整个调用堆栈,暂停程序并打印堆栈跟踪,以便找到错误并修复。
捕获这类错误打破了处理流程并且代码中有 bug。不要在这里使用错误处理代码,而是需要到导致该错误出现的地方修复你的代码。
rethrow
来重新抛出捕获的异常
要 使用 Linter rule: use_rethrow_when_possible
如果你想重新抛出一个异常,推荐使用 rethrow
语句。
rethrow
保留了原来的异常堆栈信息。而 throw
会把异常堆栈信息重置为最后抛出的位置。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
异步
Dart 具有几个语言特性来支持异步编程。下面是针对异步编程的最佳实践。
推荐 使用 async/await 而不是直接使用底层的特性
显式的异步代码是非常难以阅读和调试的,即使使用很好的抽象(比如 future)也是如此。这就是为何 Dart 提供了 async
/await
。这样可以显著的提高代码的可读性并且让你可以在异步代码中使用语言提供的所有流程控制语句。
Future<int> countActivePlayers(String teamName) async {
try {
var team = await downloadTeam(teamName);
if (team == null) return 0;
var players = await team.roster;
return players.where((player) => player.isActive).length;
} catch (e) {
log.error(e);
return 0;
}
}
Future<int> countActivePlayers(String teamName) {
return downloadTeam(teamName).then((team) {
if (team == null) return Future.value(0);
return team.roster.then((players) {
return players.where((player) => player.isActive).length;
});
}).catchError((e) {
log.error(e);
return 0;
});
}
async
不要 在没有有用效果的情况下使用 当成为习惯之后,你可能会在所有和异步相关的函数使用 async
。但是在有些情况下,如果可以忽略 async
而不改变方法的行为,则应该这么做:
Future<int> fastestBranch(Future<int> left, Future<int> right) {
return Future.any([left, right]);
}
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
return Future.any([left, right]);
}
下面这些情况 async
是有用的:
-
你使用了
await
。 (这是一个很明显的例子。) -
你在异步的抛出一个异常。
async
然后throw
比return new 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';
考虑 使用高阶函数来转换事件流 (stream)
This parallels the above suggestion on iterables. Streams support many of the same methods and also handle things like transmitting errors, closing, etc. correctly.
避免 直接使用 Completer
很多异步编程的新手想要编写生成一个 future 的代码。而 Future 的构造函数看起来并不满足他们的要求,然后他们就发现 Completer 类并使用它:
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}
Completer 是用于两种底层代码的:新的异步原子操作和集成没有使用 Future 的异步代码。大部分的代码都应该使用 async/await 或者 Future.then()
,这样代码更加清晰并且异常处理更加容易。
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
Future<bool> fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains('bear');
}
Future<T>
对 FutureOr<T>
参数进行测试,以消除参数可能是 Object
类型的歧义
要 使用 在使用 FutureOr<T>
执行任何有用的操作之前,通常需要做 is
检查,来确定你拥有的是 Future<T>
还是一个空的 T
。如果类型参数是某个特定类型,如 FutureOr <int>
,使用 is 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>
,它会错误地将其视为一个空的同步对象值。