- 库
- [DO]
part of
指令之后使用字符串 - [DON'T] 不要在你的库的src目录下引入其他库
- [PREFER] 库lib目录下的引入请使用相对路径
- [DO]
- 字符串
- [DO] 使用
adjacent strings
串联字符串而不是使用+
号 - [PREFER] 使用模板字符串来拼接值和字符而不是用
+
拼接 - [AVOID] 在不需要使用大括号时省略大括号
- [DO] 使用
- 集合
- [DO] 尽量使用字面量定义集合
- [DON'T] 不要使用
.length
去判断集合是否为空 - [CONSIDER] 使用高阶函数对集合进行转换处理
- [AVOID] 避免在
Iterable.forEach()
里写函数 - [DON'T] 不要使用
List.from()
除非你想转换集合类型 - [DO] 使用
whereType()
去过滤集合类型 - [DON'T] 当其他操作符可以转换类型时不要使用
cast()
- [AVOID] 避免使用
cast()
- 函数
- [DO] 直接声明函数而不是将lambda函数赋值给一个变量
- [DON'T]
lambda
表达式尽量简洁
- 参数
- [DO] 使用
=
为吗命名参数设置默认值 - [DON'T] 不要将默认值显式设置为
null
- [DO] 使用
- 变量
- [DON'T] 不要将初始化变量显式设置为
null
- [AVOID] 避免存储你可以计算的值
- [DON'T] 不要将初始化变量显式设置为
- 成员
- [DON'T] 不要在不必要的时候设置
getter
和setter
- [PREFER] 使用
final
声明一个只读属性 - [CONSIDER] 对于一个简单属性的获取使用
=>
- [DON'T] 不要在不必要的时候使用
this
- [DO] 尽量在初始值声明时赋值初始值
- [DON'T] 不要在不必要的时候设置
- 构造函数
- [DO] 尽量使用简洁的构造函数声明方式
- [DON'T] 不要为构造函数参数声明类型
- [DO] 构造函数body为空时使用
;
而不是{}
- [DON'T] 不要使用
new
关键字声明实例 - [DON'T] 不要重复冗余的声明
const
- 错误处理
- [AVOID] 避免在没有条件控制下捕捉错误
- [DON'T] 不要忽略错误
- [DO] 仅仅在语法错误的情况下抛出实现
Error
的类 - [DON'T] 开发时不要对错误做处理,let's crash
- [DO] 使用
rethrow
关键词重新抛出无法处理的异常
- 异步
- [PREFER] 使用
async/await
优于传统的Future
- [DON'T] 不要在
async
没有任何作用时使用它 - [CONSIDER] 使用高阶函数处理转换流
stream
- [AVOID] 避免直接使用
Completer
类 - [DO] 当参数声明类型为
Future<T>
时候,参数可能为Object
的情况下请用Future<T>
做类型判断
- [PREFER] 使用
下面的建议可以帮助你以一致的可维护的方式在多个文件中编写程序
许多dart开发者完全不使用
part
,因为他们发现当他们的库源文件是单文件时很容易读懂整个代码。如果你选择使用part
来拆分你的库文件,Dart要求其他文件需要使用part of
显式声明所属库。因为遗留原因Dart允许part of
参数为库名,这让工具很难识别库的主文件,并且容易产生歧义。 更建议的是使用URI字符串的方式声明库主文件,就像你在其他诸如import
指令一样,下面是一个例子:
// good
library my_library;
part "some/other/file.dart";
// good
// your part file
part of "../../my_library.dart";
//bad
part of my_library;
lib
目录下的src
目录所包含的源代码对于库来说是私有实现,包维护者对其包版本应该考虑这种约定,私有实现可以随意更改而不会对包产生破坏性更新。
这意味着如果你引入了其他包的私有库/文件,非破坏性的更新也会破坏你的代码。
my_package
└─ lib
├─src
| └─ utils.dart
└─api.dart
如果api.dart想要导入utils.dart,那么应该这么做:
// good
import 'src/utils.dart';
// bad
import 'package:my_package/src/utils.dart'
其实并没有很特别的理由选择前者,主要是前者描述短一点并且我们希望保持一致
下面是一些Dart语言中处理字符串的最佳实践
Dart中你可以使用如下的方式(相邻字符串)串联字符串,这种方式可以很容易将一个超长字符串分割多行且不用一直用
+
// good
show(
'what happend in the dartlang world'
'and what can we do now ?');
// bad
show('what happend in the dartlang world'+
'and what can we do now ?');
如果你有ES6使用经验,相信你一定不会对模板字符串感到陌生,Dart也提供相同的功能
// good
'Hello , $name ! you are ${year - birth} years old';
// bad
'Hello ,'+name+' you are '+(year - birth).toString()+' years old';
// goods
'Hi , $name'
'Wear your wildest $decade's outfit'
// bad
'Hi, ${name}'
"Wear your wildest ${decade}`s outfit"
Dart提供开箱即用的集合类型:Maps,Sets,lists and queues,下面是一些最佳实践。
有两种方式定义一个空数组:
[]
和List()
,同样的有三种方式定义Linked HashMap:{}
,Map()
和LinkedHashMap()
如果你想生成固定长度集合或者一些自定义类型集合请使用构造器,其他情况使用字面量语法。
// good
var points = [];
var addresses = {};
// bad
var points = List();
var addresses = Map();
必要时你可以申明集合类型
// good
var points = <Point>[];
var addresses = <String, Addresses>{};
// bad
var points = List<Point>();
var addresses = Map<String, Addresses>();
注意这些建议不适用于这些类的命名构造函数
List.from()
,Map.fromIterable()
,这些方法有他们自己的用途。例如如果你想使用List()
创建已知内容的集合 你可以使用他们
相比于使用
.length
去判定一个集合是否为空,更建议使用阅读性更强的.isEmpty
和.isNotEmpty
。
// good
if ( list.isEmpty ) return 'this is a empty list';
if ( array.isNotEmpty ) return 'wooo, a non-empty array';
// bad
if( list.length == 0 ) return 'this is a empty list';
if( !array.isEmpty ) return 'wooo, a non-empty array';
如果你想转换集合生成新集合,请使用诸如
.map()
,.where()
等基于Iterable
的函数 如果使用for loop
方式会显得冗余并且容易产生副作用
// good
var coolBoy = Boys
.where((boy) => boy.isRich)
.where((boy) => boy.isTall)
.map((boy) => boy.name);
forEach()
函数在JS中应用广泛,不过在Dart中想要遍历一个对象惯用的方法是使用for-in
的方式
// good
for ( var i in people ) {
// your function here
}
// bad
people.forEach((i) {
// your function here
});
有一种情况例外那就是当我们的处理函数已存在(无需再次申明),并可以接受元素作为参数
// good
people.forEach(print);
给你一个
Iterable
,这里有两种方式生成新的List
(包含一样的子元素)
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
上面两种方式明显的区别是第一种方式简短一点,重要的不同之处是第一种会保留参数类型
// good
// Creates a List<int>
var iterable = [1,2,3]
// Prints "List<int>"
print(iterable.toList().runtimeType);
// bad
// 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); // List<int>
如果你的集合包含多种类型,你只想获取int类型,你可以使用
.where()
// bad
var objs = [1, '2', 3, '4'];
var ints = objects.where((e) => e is int);
有时候返回的类型可能不是你想要的,你会使用
.cast()
转换类型
// bad
var objs = [1, '2', 3, '4'];
var ints = objs.where((e) => e is int).cast<int>();
上面的方式虽然解决了问题,却使用了两层处理产生了冗余的运行时判断,幸运的是Dart核心库现在提供了
whereType()
方法解决这个问题。
// good
var objs = [1, '2', 3, '4'];
var ints = objs.whereType<int>();
使用
whereType()
很简洁,可以生成自己想要的类型而不用多做一层处理
我们在处理
iterable
或者stream
时经常需要做类型转换,经理不要使用cast()
做类型转换
// good
var stuff = <dynamic>[1,2];
var ints = List<int>.from(stuff)
// bad
var stuff = <dynamic>[1,2];
var ints = stuff.toList().cast<int>();
在使用
map()
等方法时也可以省略掉cast()
的使用
// good
var stuff = <dynamic>[1,2];
var re = stuff.map<double>((n) => 1 / n);
// bad
var stuff = <dynamic>[1,2];
var re = stuff.map((n) => 1 / n).cast<double>();
避免使用
cast()
,用以下方式代替
- 声明正确的类型 在集合声明时就指定正确的类型
- 在获取元素时转换类型 如果你在遍历元素,在处理元素之前就使用
as
转换类型 - 使用
List.from()
做转换 如果你需要获取集合中的大多数元素,请使用List.from()
声明正确的类型的例子
// good
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
//bad
List<int> singletonList(int value) {
var list = [];
list.add(value);
return list.cast<int>();
}
在获取元素时转换类型的例子
// good
void printEvens(List<Object> objects) {
for (var n in objects) {
if((n as int).isEven) print(n);
}
}
// bad
void printEvens(List<Object> objects) {
for (var n in objects.cast<int>()) {
if (n.isEven) print(n);
}
}
使用
List.from()
做转换的例子
// good
int median(List<Object> objects) {
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}
// bad
int median(List<Object> objects) {
var ints = objects.cast<int>();
inst.sort();
return ints[ints.length ~/ 2];
}
有时候
cast()
也是正确选择,但是考虑到这个方法使用有一定风险-操作可能会很慢且有时候会在运行时失败,不建议使用
在Dart中函数也是对象(Object)
现代语言都会提到嵌套函数和闭包的重要性,在一个函数中定义另一个函数是很常见的,在很多实例中这种类型的函数会被作为回调函数立即使用 且声明时不用命名。但是请直接声明函数而不是将lambda函数赋值给一个变量。
// good
void main() {
localFunction() {
// ...
}
}
// bad
void main() {
var localFunction = () {
...
};
}
使用已有功能的函数作为closure,而不是再一次去重复实现该功能
// good
names.forEach(print);
// bad
names.forEach((name) {
print(name);
})
因为历史遗留原因,Dart允许
:
和=
为命名参数,为了和可选位置参数保持一致,请使用=
// good
void insert(Object item, {int at = 0}) { ... }
// bad
void insert(Object item, {int at: 0}) { ... }
如果你创建了一个可选参数但是没有给予默认值,Dart会为你的参数设置默认值为
null
,所以你不用再做处理
// good
void error([String message]) {
stderr.write(message ?? '\n');
}
// bad
void error([String messgae = null]) {
stderr.write(messgae ?? '\n');
}
下面是一些在Dart中如何使用变量的最佳实践
在Dart中未赋值的变量都会被初始化为
null
,所以添加= null
是多余不必要的。
// good
int _nextId;
class LazyId {
int _id;
int get id {
if (_nextId == null) _nextId = 0;
if (_id == null) _id = _nextId++;
return _id;
}
}
// bad
int _nextId = null;
class LazyId {
int _id = null;
int get id {
if (_nextId == null) _nextId = 0;
if (_id == null) _id = _nextId++;
return _id;
}
}
当设计一个类时,你可能经常会在初始化时计算所有的属性并存储它们
// bad
class Circle {
num radius;
num area;
num circumference;
Circle(num radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
上面的代码有两个糟糕的地方:首先这很消耗内存,更糟糕的是Circle类的
radius
是可变的,当改变radius
值后我们获取的area
和circumference
还是之前的计算值,这就会导致错误。为了保证准确性我们可能会像下面这样做:
// bad
class Circle {
num _radius;
num get radius => _radius;
set radius(num value) {
_radius = value;
_recalculate();
}
num _area;
num get area => _area;
num _circumference;
num get circumference => _circumference;
Circle(this._radius) {
_recalculate();
}
void _recalculate() {
_area = pi * _radius * _radius;
_circumference = pi * 2.0 * _radius;
}
}
上面的解决方法难以阅读,表达性极差,取而代之的实现方式应该如下:
// good
class Circle {
num radius;
Circle(this.radius);
num get area => pi * radius * radius;
num get circumference => pi * 2.0 * radius;
}
这样代码就显得很简洁,更少的内存占用,更少的错误产生。它只存储了必要的数据。在一些案例里,你可能需要 存储一些运算量较大的值,请谨慎这么做并写上注释解释为什么需要这么做优化。
##成员
在Dart中Object可以有函数(方法)和数据(实例变量)类型的成员,下面是一些最佳实践
在Java或者C#中,成员变量通常是隐藏在
getter
和setter
之后的,你需要写很多get/set的样板代码。Dart没有这样的限制, 声明的变量会自动设置getter
和setter
。
// good
class Box {
var contents;
}
// bad
class Box {
var _contents;
get contents => _contents;
set contents(value) {
_contents = value;
}
}
如果你想设置一个只读属性变量,一个简单的解决办法是使用
final
标识变量
// good
class Box {
final contents = [];
}
// bad
class Box {
var _contents;
get contents => _contents;
}
当然你或许需要在类的构造器外部去设置变量值,你可能需要
private field public getter
设计模式,这种情况下 第二种方法更合适你
当计算表达式足够简单时使用
=>
,这种做法非常适合获取只需要简单计算的成员变量
// good
double get area => (right - left) * (bottom - top);
bool isReady(num time) => minTime == null || minTime <= time;
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
当我们的处理较为复杂的时候,不建议使用箭头函数这会让代码难以阅读。
// good
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;
}
// bad
Treasure openChest(Chest chest, Point where) =>
_opened.containsKey(chest) ? null : _opened[chest] = Treasure(where)
..addAll(chest.contents);
你也可以在设置
setter
时使用=>
// good
num get x => center.x;
set x(num value) => center = Point(value, center.y);
JS必须使用
this
来指向类去获取成员,不过Dart和Java C++等语言一样没有这种限制 唯一需要使用this
的情况是你在成员函数里需要获取成员变量的时候
// good
class Box {
var value;
void clear() {
update(null);
}
void update(value) {
this.value = value;
}
}
// bad
class Box {
var value;
void clear() {
this.update(null);
}
void update(value) {
this.value = value;
}
}
注意构造初始化时参数赋值是不需要
this
的
// good
class Box extends BaseBox {
var value;
Box(value)
: value = value,
super(value);
}
这看起来很意外,但是这种语法是可以正常工作的。
如果成员变量不依赖构造函数,那么它可以且应该在声明时初始化,这会让代码更加简洁并且避免了在多构造函数情况下忘记初始化
// good
class Folder {
final String name;
final List<Document> contents = [];
Folder(this.name);
Folder.temp() : name = 'temporary';
}
// bad
class Folder {
final String name;
final List<Document> contents;
Folder(this.name) : contents = [];
Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
}
当然如果成员变量依赖构造函数参数,上面所说的就不适用了。
成员变量初始化可以直接利用构造参数
// good
class Point {
num x, y;
Point(this.x, this.y);
}
// bad
class Point {
num x, y;
Point(num x, num y) {
this.x = x;
this.y = y;
}
}
如果构造函数参数使用了
this
去初始化成员变量,参数类型会自动匹配成员变量类型
// good
class Point {
int x, y;
Point(this.x, this.y);
}
// bad
class Point {
int x, y;
Point(int this.x, int this.y);
}
在Dart中,如果构造函数体为空请用冒号结尾
// good
class Point {
int x, y;
Point(this.x, this.y);
}
// bad
class Point {
int x, y;
Point(this.x, this.y) {}
}
Dart2让
new
关键词成为可选项,这样让代码更加简洁
// good
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(
child: Text('Increment'),
),
Text('Click!'),
],
);
}
// bad
Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(
child: new Text('Increment'),
),
new Text('Click!'),
],
);
}
在状态为
const
的上下文环境中,const
是隐式的可以省略
const
状态的集合const
状态的构造函数- 元数据注解
const
变量的初始化构造方法switch
表达式case
和:
之间的区域
// good
const primaryColors = [
Color("red", [255, 0, 0]),
Color("green", [0, 255, 0]),
Color("blue", [0, 0, 255]),
];
// bad
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使用异常描述你的程序错误,下面是一些捕获和处理异常的最佳实践
[TODO]
[TODO]
[TODO]
[TODO]
当捕捉到的异常无法处理想要重新抛出异常时,请使用
rethrow
关键词而不是throw
,因为rethrow
会提供完整的异常调用栈。 而throw
只提供抛出位置的调用栈。
// good
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
// bad
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
Dart原生支持异步编程,一下是一些异步编程的最佳实践
异步代码是众所周知的难以调试,即使是使用了一些比较好的抽象例如
Future
。使用async/await
语法可以提高代码的阅读性, 作用类似于JS中async/await
之余Promise
。
// good
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;
}
}
// bad
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
,在不需要的时候不要使用它
// good
Future afterTwoThings(Future first, Future second) {
return Future.wait([first, second]);
}
// bad
Future afterTwoThings(Future first, Future second) async {
return Future.wait([first, second]);
}
async
在以下几种情况下是很有用的:
- 你需要使用
await
- 你需要返回一个异步错误,
async
加throw
要比return Future.error(...)
简洁 - 你需要返回值且值被隐式的包装为
Future
,async
要比Future.value(...)
简洁
// good
Future usesAwait(Future later) async {
print(await later);
}
Future asyncError() async {
throw 'Error!';
}
Future asyncValue() async => 'value';
和对
Iterable
的处理建议一样,stream
也提供很多同样的方法,并且可以准确处理传输失败,关闭等事件
许多异步编程新手想要创建
Future
,Future构造函数貌似不能满足他们的需求,他们最终使用Cpmpleter
来完成任务。
// bad
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}
相较于使用
Future.then()
或者async/await
,Completer看起来更加复杂和难以处理
// good
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
// good
Future<bool> fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains('bear');
}
[TODO]
// good
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 as T;
}
}
// bad
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;
}
}