一、 变量
对于局部变量,必须遵循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使用率。 我们是否知道我们有一个值得权衡的性能问题?
更糟糕的是,代码是错误的。 缓存的问题是无效-您如何知道缓存何时过期并且需要重新计算? 在这里,即使半径是可变的,我们也永远不会这样做。 您可以分配一个不同的值,面积和周长将保留以前的值,现在是不正确的值。
为了正确处理缓存失效,我们需要这样做:
差的写法
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中包装字段。
在Java和C#中,通常将所有字段都隐藏在getter和setter(或C#中的属性)后面,即使实现只是转发到该字段。 这样,如果您需要在这些成员中进行更多工作,则无需触摸呼叫callsites。 这是因为调用getter方法不同于访问Java中的字段,并且访问属性与访问C#中的原始字段不是二进制兼容的。
Dart没有此限制。 字段和getters/setters是完全无法区分的。 您可以在一个类中公开一个字段,然后将其包装在一个getter和setter中,而无需触摸使用该字段的任何代码。
好的写法
class Box {
var contents;
}
差的写法
class Box {
var _contents;
get contents => _contents;
set contents(value) {
_contents = value;
}
}
优先使用final
字段来创建只读属性。
如果您有一个外部代码应该可以看到但不能分配给它的字段,那么在许多情况下都可以使用的简单解决方案是简单地对其进行标记final
。
好的写法
class Box {
final contents = [];
}
差的写法
class Box {
var _contents;
get contents => _contents;
}
考虑=>
用于简单成员。
除了=>
用于函数表达式外,Dart还允许您使用它定义成员。该样式非常适合只计算并返回值的简单成员。
好的写法
double get area => (right - left) * (bottom - top);
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
您也可以=>
在不返回值的成员上使用。当setter较小并且具有使用的对应getter时,这是惯用的=>
。
好的写法
num get x => center.x;
set x(num value) => center = Point(value, center.y);
不要使用this. 除了重定向到命名构造函数或避免遮盖。
JavaScript需要显式的。 引用对象上当前正在执行其方法的成员,但是Dart(例如C ++,Java和C#)没有该限制。
您只需要使用this.一次是当具有相同名称的局部变量遮盖了您要访问的成员时:
差的写法
class Box {
var value;
void clear() {
this.update(null);
}
void update(value) {
this.value = value;
}
}
好的写法
class Box {
var value;
void clear() {
update(null);
}
void update(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 {
var value;
Box(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
。
当然,如果字段取决于构造函数参数,或者由不同的构造函数以不同的方式初始化,则此准则不适用。
三、构造函数
尽可能使用初始化形式。
许多字段是直接从构造函数参数初始化的,例如:
差的写法
class Point {
double x, y;
Point(double x, double y)
: x = x,
y = y;
}
好的写法
class Point {
double x, y;
Point(this.x, this.y);
}
late
构造函数初始值设定项列表将要使用时不要使用。
因为null的安全性要求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;
}
}
初始化程序列表使您可以访问构造函数参数,并可以在读取字段之前对其进行初始化。 因此,如果可以使用初始化列表,那总比推迟现场工作并失去一些静态安全性和性能要好。
要使用;
代替{}
空的构造函数体
在Dart中,带有空主体的构造函数可以仅用分号终止。(实际上,它是const构造函数所必需的。)
好的写法
class Point {
double x, y;
Point(this.x, this.y);
}
差的写法
class Point {
double x, y;
Point(this.x, this.y) {}
}
不要使用new
。
Dart 2使new
关键字成为可选关键字。即使在Dart 1中,其含义也从未明确,因为工厂构造函数意味着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。
在表达式必须为常量的情况下,const关键字是隐式的,不需要编写也不需要编写。 这些上下文是其中的任何表达式:
const集合文字。
const构造函数调用
元数据注释。
const变量声明的初始化程序。
转换case表达式-在case后面紧跟在:之前的部分,而不是case的正文。
(默认值未包含在此列表中,因为Dart的未来版本可能会支持非const默认值。)
基本上,在任何地方写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]),
];
四、错误处理
避免不带on子句的捕获。
一个没有on限定词的catch子句捕获try块中的代码抛出的任何内容。“神奇宝贝异常(术语)”处理很可能不是您想要的。您的代码是否正确处理了*Error或OutOfMemoryError?如果错误地将错误的参数传递给该try块中的方法,是要让调试器将您指向该错误,还是希望吞噬有用的ArgumentError?您是否想在代码中捕获所有抛出断言的AssertionErrors的assert()语句?
答案可能是“否”,在这种情况下,您应该过滤捕获的类型。在大多数情况下,应该有一个on子句,将您限制在您知道并正确处理的运行时失败的类型上。
在极少数情况下,您可能希望捕获任何运行时错误。这通常是在框架或低级代码中,它们试图隔离任意应用程序代码以免引起问题。即使在这里,捕获Exception通常也比捕获所有类型更好。异常是所有运行时错误的基类,并且排除指示代码中编程错误的错误。
没有on子句的情况下,请勿丢弃捕获的错误。
如果您确实确实需要捕获可能从代码区域抛出的所有内容,请对所捕获的内容进行处理。记录它,将其显示给用户或重新扔掉,但不要静默丢弃。
不要只对程序错误抛出实现Error的对象。
Error类是程序错误的基类。当抛出该类型的对象或其子接口之一(例如ArgumentError)时,意味着代码中存在错误。当您的API想要向调用者报告错误地使用了该API时,抛出错误将清楚地发送该信号。
相反,如果异常是某种运行时故障,并不表示代码中存在错误,则抛出错误会产生误导。而是抛出核心Exception类之一或某些其他类型。
不要明确捕获Error或实现它的类型。
这是从上面得出的。由于“错误”指示代码中存在错误,因此它应展开整个调用堆栈,暂停程序并打印堆栈跟踪,以便您可以找到并修复该错误。
捕获这些类型的错误会中断该过程并掩盖该错误。事实发生后,与其添加错误处理代码来处理此异常,不如回过头来修复导致该异常首先被抛出的代码。
请使用rethrow
来重新抛出捕获的异常。
如果决定重新抛出异常,则最好使用rethrow语句,而不是使用throw抛出相同的异常对象。重新抛出保留了异常的原始堆栈跟踪。另一方面,throw将堆栈跟踪重置为最后抛出的位置。
差的写法
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
好的写法
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
五、异步
优选使用原始特征async/await。
众所周知,即使使用诸如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
当它没有有用的效果时,请不要使用。
容易养成在与异步有关的任何函数上使用异步的习惯。 但在某些情况下,这是多余的。 如果可以在不更改功能行为的情况下忽略异步,请执行此操作。
好的写法
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]);
}
异步有用的情况包括:
您正在使用等待。 (这是显而易见的。)
您正在异步返回错误。 异步然后抛出比返回Future.error(...)短。
您正在返回一个值,并且希望将来将其隐式包装。 异步比Future.value(...)短。
好的写法
Future<void> usesAwait(Future<String> later) async {
print(await later);
}
Future<void> asyncError() async {
throw 'Error!';
}
Future<void> asyncValue() async => 'value';
考虑使用高阶方法来转换流。
这与上述关于可迭代对象的建议相类似。 流支持许多相同的方法,并且还正确处理传输错误,关闭等问题。
避免直接使用Completer。
许多异步编程新手都想编写能创造未来的代码。 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:新的异步原语,以及与不使用特征的异步代码进行接口。 其他大多数代码都应使用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');
}
FutureOr <T>的参数类型可能是Object时,请对Future <T>进行测试。
在对FutureOr <T>做任何有用的事情之前,通常需要先进行一次is检查,看看是否有Future <T>或裸T。如果type参数是某些特定类型,例如FutureOr <int> ,无论您使用哪个测试,int还是Future <int>都无关紧要。 之所以起作用,是因为这两种类型是不相交的。
但是,如果值类型是Object或可能用Object实例化的类型参数,则两个分支重叠。 Future <Object>本身实现Object,所以Object还是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>传递给它,则会错误地将其视为裸露的同步值。