阅读 Effective Dart 时做的一些笔记。
Style Guide
好代码一定是遵循良好代码风格的代码。前后一致的命名、排序和格式化使得代码具有更高的可读性。在整个 Dart 生态中维持一个规范统一的代码风格,可以使得程序员之间分享彼此的代码时,更容易阅读和互相学习。
Identifiers
Dart 中标识符有三种类型:
- UpperCamelCase:大驼峰命名法,每个单词首字母大写。
- lowerCamelCase:小驼峰命名法,除了首个单词,其余每个单词首字母大写。
- lowercase_with_underscores:带下划线的小写命名法,每个单词小写,用下划线分割单词。
DO name types using UpperCamelCase
.
类名,枚举类,typedef
,注解等的命名都应该采用大驼峰命名法。
class SliderMenu { ... }
enum Status { ... }
typedef Predicate = bool Function(T value);
@Foo(anArg)
class A { ... }
#### DO name extensions using `UpperCamelCase`.
和类型命名一样,扩展方法也应该使用大驼峰命名法。
```dart
extension MyFancyList on List { ... }
DO name libraries, packages, directories, and source files using lowercase_with_underscores
.
库名,包名,文件和文件夹名,都应该使用小写+下划分隔符命名。
library my_library;
export 'global_constants.dart';
export 'src/common_util/screen_util.dart';
DO name import prefixes using lowercase_with_underscores
.
导入包名使用别名时,用小写+下划分隔符。
import 'package:angular_components/angular_components' as angular_components;
DO name other identifiers using lowerCamelCase
.
顶层方法,变量,类的成员,变量,参数等,都应该使用小驼峰命名法。
var httpRequest;
void alignItems(bool clearItems) {
// ...
}
PREFER using lowerCamelCase
for constant names.
推荐使用小驼峰命名常量、枚举类。
const defaultTimeout = 1000;
final urlScheme = RegExp('^([a-z]+):');
class Dice {
static final numberGenerator = Random();
}
DO capitalize acronyms and abbreviations longer than two letters like words.
大于两个字符的缩写均当做普通单词使用。
class HttpConnection {} // bad: HTTPConnection
class DBIOPort {} // bad: DbIoPort
class TVVcr {}
class MrRogers {}
var httpRequest = ...
var uiHandler = ...
Id id;
PREFER using _
, __
, etc. for unused callback parameters.
推荐使用 _
命名未使用的回调参数。
futureOfVoid.then((_, __, ___) {
print('Operation complete.');
});
DON’T use a leading underscore for identifiers that aren’t private.
不要使用下划线作为标识符的起始字符,只有私有变量和私有函数才可以使用下划线开头。
var _notVisible;
_initContentForCaseA() {
...
}
DON’T use prefix letters.
不要使用前缀字符。
var defaultTimeout; // bad: kDefaultTimeout
Ordering
为了保持一个 Dart 文件的整洁性,我们应该保证每段代码都按照特定的顺序排列,并且每个区域都被空白行分割。
DO place “dart:” imports before other imports.
import 'dart:async';
import 'dart:html';
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
DO place “package:” imports before relative imports.
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
import 'util.dart';
DO specify exports in a separate section after all imports.
import 'src/error.dart';
import 'src/foo_bar.dart';
export 'src/error.dart';
DO sort sections alphabetically.
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
import 'package:gold/gold.dart';
Formatting
为了使得代码具有良好的可读性,我们需要格式化 Dart 代码。
DO format your code using dart format
.
Dart 为我们提供了 dart format 工具格式化代码,具体见相关文档。
CONSIDER changing your code to make it more formatter-friendly.
在某些场景下,格式化可能会失效,比如很长的标识符、嵌套很深的表达式、多种操作符的混合使用等,我们应该考虑修改代码使得代码更加容易被格式化。
AVOID lines longer than 80 characters.
单行代码越长越难读,考虑换行,或者将一些特别长的标识符简写。
DO use curly braces for all flow control statements.
哪怕只有流程语句中只有一个表达式也应该使用花括号。
Documentation Guide
良好的文档和注释可以使得阅读你的代码的人(包括未来的你)更容易理解你的代码。简洁、精确的注释可以节约一个人大量的时间和精力去理解看懂一段代码所需要的上下文。虽然好的代码是自解释的,但是大多数时候我们都应该写更多的注释而不是更少的注释。
Comment
DO format comments like sentences.
应该像写普通的句子一样写注释,注释尽量使用英文,首字符前加空格,首字母大写,每一句都应该使用标点。如果使用中文,则应该在中英文之间使用空格分割。
// Not if there is nothing before it.
if (_chunks.isEmpty) return false;
// 尽量减少使用中文注释,即使使用也要 use space 分割中英文字符。
if (_chunks.specialCases) return true;
DON’T use block comments for documentation.
Dart 中不推荐使用块注释。
greet(name) {
// Assume we have a valid name.
print('Hi, $name!');
// bad:
/* Assume we have a valid name. */
print('Hi, $name!');
}
Doc comments
文档注释 ///
可以用于方便地生成文档页,主要通过 dartdoc 生成。
DO use ///
doc comments to document members and types.
对于类和成员变量变量,使用 ///
注释,这样 dartdoc 才能找到它们并解析成文档。
/// The number of characters in this chunk when unsplit.
int get length => ...
PREFER writing doc comments for public APIs.
不用给每个类,成员变量写注释,但是至少关键部分应该写文档注释。
CONSIDER writing a library-level doc comment.
最好在 Dart library 的入口提供文档注释,比如 library 的主要功能、术语解释、样本代码、常用类和方法的链接、外部引用资源等。
CONSIDER writing doc comments for private APIs.
一些重要的私有方法也应该提供文档注释,方便库的使用者理解你的代码。
DO start doc comments with a single-sentence summary.
每个文档注释的开头应该使用简单、准确的一句话作为总结概括。
DO separate the first sentence of a doc comment into its own paragraph.
第一句总结之后使用空白行分割,可以使得文档注释更易读。
AVOID redundancy with the surrounding context.
为类的成员和方法写注释时,避免写得太过啰嗦,因为读者对类的基本用途和上下文已经有了解了。
PREFER starting function or method comments with third-person verbs.
使用第三人称动词作为方法注释的开头,因为方法一般都是执行一项任务,这样可以让读者更快了解方法的用途。
PREFER starting variable, getter, or setter comments with noun phrases.
使用名词作为变量注释的开头,因为变量一般代表一条数据。
PREFER starting library or type comments with noun phrases.
使用名词作为库和类型注释的开头,同样库和类型是一种对象火种功能的抽象。
CONSIDER including code samples in doc comments.
复杂的代码中如果包含示例代码有助于读者理解你的代码。
DO use square brackets in doc comments to refer to in-scope identifiers.
文档注释中使用中括号引用当前上下文中代码的链接。
/// Throws a [StateError] if ...
/// similar to [anotherMethod()], but ...
/// Similar to [Duration.inDays], but handles fractional days.
/// To create a point from polar coordinates, use [Point.polar()].
DO use prose to explain parameters, return values, and exceptions.
Java 中一般使用 @param
, @returns
, @throws
等注解来注释参数和返回值等,在 Dart 中不要这么做,推荐使用直白文字的叙述方法的功能,参数以及特殊的地方。
DO put doc comments before metadata annotations.
文档注释应该在注解之前。
/// A button that can be flipped on and off.
@Component(selector: 'toggle')
class ToggleComponent {}
Markdown
Dart 中支持大多数常见的 markdown 语法。
AVOID using markdown excessively.
When in doubt, format less. Formatting exists to illuminate your content, not replace it. Words are what matter. (说得太经典了,拿小本本记下来(๑•͈ᴗ•͈))
AVOID using HTML for formatting.
大多数情况下,应该只使用 markdown 语法就够了。
PREFER backtick fences for code blocks.
我们可以使用 inline code
或者 ``` code blocks,如果是小段的代码可以使用前者,如果是大段的代码块,推荐使用后者。
Writing
作为程序员,虽然我们每天主要和计算机打交道,但是我们写得绝大多数代码都是给人读的,写代码需要练习,写作也同样需要练习。
这里介绍一些写作技巧,更多关于技术写作的技巧,推荐阅读:Technical writing style.
PREFER brevity.
保证你的文字是清晰和精准的,同时保持简洁。
AVOID abbreviations and acronyms unless they are obvious.
尽可能少使用缩写,除非是那种人人都知道的,比如 "i.e.", "e.g.", "etc"。
PREFER using “this” instead of “the” to refer to a member’s instance.
当需要代指当前类的实例是时候,使用 this 代替 the。
class Box {
/// True if this box contains a value.
bool get hasValue => _value != null;
}
Usage Guide
Libraries
DO use strings in part of
directives.
在 Dart 中,我们可以使用 part of
将代码分离到另一个文件中,然后使用 part
引用这部分分离出去的代码,推荐的做法是,直接使用 URI 链接到指定的文件,而不是只指定库的名字。
part of '../../my_library.dart';
// bad:
part of my_library;
DON’T import libraries that are inside the src
directory of another package.
我们应该直接导入库,而不是导入库中的某个文件或者目录,因为库作者可能对目录结构做修改。
DON’T allow an import path to reach into or out of lib
.
比如使用相对路径引用 lib 之外的某个文件,或者 lib 之外的某个文件(比如 test 文件夹)导入 lib 内的文件,这两种情况都应该避免,应该只使用包导入的方式导入依赖。
import 'package:my_package/api.dart';
// bad:
import '../lib/api.dart';
PREFER relative import paths.
如果无法使用包导入,才使用相对路径的方式导入。
test/api_test.dart:
import 'test_utils.dart'; // 在当前同一个目录结构下,可以使用相对路径
Null
DON’T explicitly initialize variables to null
.
Dart 会自动为可为空的变量赋值为 null,而不可为空的变量在初始化之前被使用的话会报错。所以没必要为变量赋空值。
DON’T use an explicit default value of null
.
与上一条类似,如果你为一个可选位置参数的值可为空,那么 Dart 会为它自动赋值为空值,没必要手动赋值为空。
PREFER using ??
to convert null
to a boolean value.
使用 ??
将空值转换为布尔值,好处更简洁易懂。看例子:
// If you want null to be false:
if (optionalThing?.isEnabled ?? false) {
print("Have enabled thing.");
}
// If you want null to be true:
if (optionalThing?.isEnabled ?? true) {
print("Have enabled thing or nothing.");
}
// 第二种情况等同于下面这种写法,所以使用 ?? 明显可以简化写法
if (optionalThing?.isEnabled == null || optionalThing!.isEnabled!) {
print("Have enabled thing or nothing.");
}
AVOID late
variables if you need to check whether they are initialized.
使用 late
关键字可以让我们延迟初始化某些变量,但是这样我们就没法判断某个变量是否初始化了,当需要做这样的判断时候,最好的做法是不要使用 late
关键字,而是使用 nullable 变量。
// 使用 late 关键字
late Type a;
bool initialized = false;
initialize() {
a = Type('A');
initialized = true;
}
doSomeWorkEnsureInitialized() {
if (a == null) {
if (!initialized) {
intialize();
}
}
doWork();
}
// 不使用 late 关键字
Type? a;
doSomeWorkEnsureInitialized() {
if (a == null) {
initialize();
}
doWork();
}
CONSIDER copying a nullable field to a local variable to enable type promotion.
Dart 中有类型提升的机制,但是只适用于本地变量,因此,对于可为空的成员变量我们应该先将其拷贝成本地变量,然后再使用。
final Response? response;
@override
String toString() {
var 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).";
}
但是要注意如果本地变量发生变化,要将其重新赋值到成员变量上。而且成员变量有可能在被拷贝之后发生了变化,则拷贝的本地变量已经「过时」了。
Strings
DO use adjacent strings to concatenate string literals.
Dart 中不需要使用 + 来连接两个字符,像 C 和 C++ 中一样,相邻的字符串会自动被拼接成同一个字符串。
print('content1,''content2,');
print('Some very very long string text which cannot be put into one line, '
'but can be put into adjacent line to be concatenated together without +');
PREFER using interpolation to compose strings and values.
'Hello, $name! You are ${year - birth} years old.'; // good
'Hello, ' + name + '! You are ' + (year - birth).toString(); // bad!
Collections
DO use collection literals when possible.
创建集合的时候使用字面量表达式,而不是默认构造器,因为字面量表达式还支持扩展表达式和集合 if 和集合 for 的操作。
// good:
var points = [];
var addresses = {};
var counts = {};
// bad:
var points = List.empty(growable: true);
var addresses = Map();
var counts = Set();
DON’T use .length
to see if a collection is empty.
使用 isEmpty
和 isNotEmpty
代替 .length
。
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
AVOID using Iterable.forEach()
with a function literal.
使用 for..in 代替 forEach()。
// good:
for (var person in people) {
...
}
// bad:
people.forEach((person) {
...
});
DON’T use List.from()
unless you intend to change the type of the result.
使用 iterable.toList()
代替 List.from(iterable)
。
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
只有在需要修改集合类型的时候才使用 List.from()
。
var numbers = [1, 2.33, 4]; // List.
numbers.removeAt(1); // Now it only contains integers.
var ints = List.from(numbers);
DO use whereType()
to filter a collection by type.
使用 whereType()
根据类型筛选集合。
var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType();
DON’T use cast()
when a nearby operation will do.
集合方法很多都支持泛型,所以除非必要,不要使用 cast()
进行类型转换。
var stuff = [1, 2];
var ints = List.from(stuff);
// var ints = stuff.toList().cast();
var reciprocals = stuff.map((n) => 1 / n);
// var reciprocals = stuff.map((n) => 1 / n).cast();
AVOID using cast()
.
如非必要,尽量减少使用 cast()
转换集合类型,考虑使用以下方法代替:
直接用目标类型创建集合。
遍历元素,对单个元素使用
cast()
(使用时才转换)。尽量使用
List.from()
代替cast()
。
cast()
方法会返回一个集合并且在每次操作时检查元素类型,如果你只在集合的少量元素上做操作则适合使用 cast()
方法,其它情况下,这种转换的性能开销都比较大。
Functions
DO use a function declaration to bind a function to a name.
当使用局部方法的时候,如果需要有方法名,尽量直接使用方法声明,避免使用 lambd 表达式+变量给方法命名。
void main() {
// good:
localFunction() {
...
}
// bad:
var localFunction = () {
...
};
}
DON’T create a lambda when a tear-off will do.
当使用匿名函数的时候,如果函数调用的方法所需的参数和函数的参数一致,则可以使用 tear-off 的语法,类似于 Java 中的方法引用。
// good:
names.forEach(print);
// bad:
names.forEach((name) {
print(name);
});
DO use =
to separate a named parameter from its default value.
由于历史遗留问题,Dart 中允许使用 :
为参数设置默认值,最好的做法是用 =
。
// good:
void insert(Object item, {int at = 0}) { ... }
// bad:
void insert(Object item, {int at: 0}) { ... }
Variables
DO follow a consistent rule for var
and final
on local variables.
绝大多数局部变量都不需要有类型标注,而是直接使用 var
或者 final
关键字声明。我们可以选择以下两个原则的其中一个,不要同时使用两种方式:
- 原则 A:如果是不会被重新赋值的变量,则使用
final
关键字,会被重新赋值的则使用var
关键字。 - 原则 B:所有的局部变量一律都使用
var
关键字,只有顶层变量和成员变量才使用final
关键字。
推荐原则 B,更简单,且容易遵循。
AVOID storing what you can calculate.
原因是浪费内存,推荐使用 getters 代替。
class Circle {
double radius;
Circle(this.radius);
// 不要使用成员变量来保存需要计算得到的值,使用 getters
double get area => pi * radius * radius;
double get circumference => pi * 2.0 * radius;
}
Members
Dart 中,对象的成员可以是方法或者变量。
DON’T wrap a field in a getter and setter unnecessarily.
Dart 中访问属性和访问 getters/setters 的效果是一样的,每个属性默认会自动生成 getters/setters,所以没必要手动去写 getters/setters,如果是为了使属性不可访问,则应该使用私有属性。
PREFER using a final
field to make a read-only property.
// good:
class Box {
final contents = [];
}
// bad:
class Box {
var _contents;
get contents => _contents;
}
CONSIDER using =>
for simple members.
使用箭头表达式定义成员变量,最常见的用法是用于创建 getters。
double get right => left + width;
set right(double value) => left = value - width;
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
DON’T use this.
except to redirect to a named constructor or to avoid shadowing.
只有以下几种情况下才使用 this
关键字:
- 构造器中引用成员变量;
- 构造器重定向到某个具名构造器时;
- 成员变量与局部变量或者参数同名时;
有趣的是,在构造器初始化列表中,可以区分出同名参数,所以不需要使用 this
。
class Box extends BaseBox {
var value;
Box(value)
: value = value,
super(value);
}
DO initialize fields at their declaration when possible.
如果成员属性不依赖构造器初始化,则应该尽量在声明处进行初始化。
class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}
Constructors
DO use initializing formals when possible.
在构造器参数前使用 this.
叫做 initializing formal。
class Point {
double x, y;
Point(this.x, this.y);
}
DON’T use late
when a constructor initializer list will do.
如果成员变量会在构造器中进行初始化,就应该避免将其标记为 late
。
DO use ;
instead of {}
for empty constructor bodies.
构造器方法体为空时使用 ;
结束构造器,避免使用空方法体。
DON’T use new
.
避免使用 new
关键字,同样是 Dart 的历史遗留问题。
DON’T use const
redundantly.
以下几种情况下,不需要使用 const
关键字:
- 在一个
const
的集合中; - 在一个
const
构造器中,其中每个参数都是 const 的; - 在元数据注解中;
-
const
常量的初始化器; - 在 switch..case 表达式中,case 之后 : 之前的值默认也是
const
的;
Error handling
AVOID catches without on
clauses.
如果 catch 子句没有使用 on
关键字限定捕捉的异常类型,则会捕捉所有类型的异常,这样我们就没法得到程序出错的具体原因了,到底是 stackoverflow 还是参数异常,又或者是断言条件未满足?所以,哪怕是只捕捉 Exception
也比捕捉全部异常好,Exception 代表程序运行时异常,排除了编码错误造成的异常。
DON’T discard errors from catches without on
clauses.
如果你执意捕捉所有异常,请不要丢弃异常内容,至少打印一下好吗?
DO throw objects that implement Error
only for programmatic errors.
所有的编码异常都继承自 Error
类,如果是运行时异常造成的错误,则应该抛出运行时异常,尽量在编码异常中排除掉运行时异常。
DON’T explicitly catch Error
or types that implement it.
实现了 Error 类的大多是编码异常,这类异常可以告诉我们程序出错的信息,通常不应该捕捉这类异常。
DO use rethrow
to rethrow a caught exception.
throw
会重置错误栈信息,而 rethrow
则不会。
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
Asynchrony
PREFER async/await over using raw futures.
async
/ await
语法可以提升代码的可读性,并且是的程序更容易 debug。
DON’T use async
when it has no useful effect.
虽然异步场景下优先推荐使用 async
,但是注意不要滥用 async
。满足以下条件时才推荐使用 async
:
- 使用了
await
.(废话) - 需要异步返回异常,使用
async
比使用 Future.error() 更简洁。 - 需要返回一个 Future 对象,使用
async
比使用 Future.value() 更简洁。
CONSIDER using higher-order methods to transform a stream.
Stream
和集合类似,包含一些诸如 every, any, distinct 等便捷的转换操作,避免手动转换。
AVOID using Completer directly.
Completer 是比较底层的类,应该避免使用,大多数场景下用 async
/await
就足够了。
DO test for Future
when disambiguating a FutureOr
whose type argument could be Object
.
在使用 FutureOr
之前,你通常需要先用 is
关键字判断其类型。因为 T 有可能是 Object,而 FutureOr<> 本身也是 Object,所以要用 Future
作为判断依据。
Future logValue(FutureOr value) async {
if (value is Future) {
var result = await value;
print(result);
return result;
} else {
print(value);
return value;
}
}
Design Guide
Names
DO use terms consistently.
在整个代码库中保持用相同的名称命名相同的物体,尽量使用被大众接受或者共同认可的方式命名。
pageCount // A field.
updatePageCount() // Consistent with pageCount.
toSomething() // Consistent with Iterable's toList().
asSomething() // Consistent with List's asMap().
Point // A familiar concept.
AVOID abbreviations.
尽量避免使用缩写,除非是非常常见的缩写。
PREFER putting the most descriptive noun last.
最后一个单词一定是最具描述性、最能总结该类用途的名词。
StatelessWidget
DownloadPage
AnimationController
OutlineInputBorder
CONSIDER making the code read like a sentence.
尽量使你的代码读起来可以像读文章一样易懂。
if (serviceTable.isEmpty) {
serviceTable.addAll(waitingList.where(
(customer) => customer.waitingMinutes > 30));
}
PREFER a noun phrase for a non-boolean property or variable.
尽量使用名词命名非布尔类型的属性或变量。
PREFER a non-imperative verb phrase for a boolean property or variable.
使用非祈使动词命名布尔类型的属性或变量。布尔类型的变量一般代表某种状态或者能力。
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
CONSIDER omitting the verb for a named boolean parameter.
具名参数一般可以省略动词,使用形容词。
var copy = List.from(elements, growable: true);
var copy = List.from(elements, canGrow: true); // 哪一种更好?
PREFER the “positive” name for a boolean property or variable.
对于布尔值,尽量使用有价值、有意义、有用的值作为 true 值等。
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}
PREFER an imperative verb phrase for a function or method whose main purpose is a side effect.
使用祈使动词短语命名那些具有 "side effect" 的方法名,side effect 即会改变对象的内部状态,比如属性等,或者产生一些新数据,或者会引起外部其它对象发生变化等等。
queue.removeFirst();
window.refresh();
PREFER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.
如果方法的主要用途是返回值,那么应该使用名词短语或者非祈使动词短语作为方法名。
var element = list.elementAt(3);
var first = list.firstWhere(condition);
var char = string.codeUnitAt(4);
CONSIDER an imperative verb phrase for a function or method if you want to draw attention to the work it performs.
当方法不产生任何 side effect 但是会比较消耗资源或者做一些有可能出错的操作时,也要使用祈使动词短语命名。
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();
AVOID starting a method name with get
.
大多数方法都应该直接省略 get 直接描述方法作用,比如使用 packageSortOrder()
而不是 getPackageSortOrder()
。
PREFER naming a method toXxx()
if it copies the object’s state to a new object.
如果方法的主要用途是将一个对象复制并转换为另一个对象时,尽量使用 toXxx() 命名。
list.toSet();
stackTrace.toString();
dateTime.toLocal();
PREFER naming a method asXxx()
if it returns a different representation backed by the original object.
如果方法的主要用途是基于一个对象包装转换成另一个对象时,尽量使用 asXxx() 命名。
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();
AVOID describing the parameters in the function’s or method’s name.
方法名中不要带有参数相关的信息。
// good:
list.add(element);
map.remove(key);
// bad:
list.addElement(element);
map.removeKey(key);
只有在为了区分多种功能类似的方法时才可以忽略该原则:
map.containsKey(key);
map.containsValue(value);
DO follow existing mnemonic conventions when naming type parameters.
使用现有的助记规则命名类型参数,比如 E 代表集合中的 Element,K/V 代表 Map 的 key/value,R 代表类或方法的 return 值,其它情况使用 T/S/U/N/E 等。
Libraries
Dart 中使用下划线 __ 代表成员是库私有的,这不仅仅是约定俗成的,更是在语法层面做出的规定。
PREFER making declarations private.
对于库的作者而言,这点尤为重要,要尽可能减少暴露接口给库的使用者,只暴露必须使用到的接口。
CONSIDER declaring multiple classes in the same library.
Dart 中每个文件都是一个 library,但是不像 Java 等语言一个文件通常只能代表一个类,Dart 中可以在相同的 library 中包含多个类、顶层变量和方法,只要这些类、变量和方法的确相互联系并且构成一个功能模块就可以了。
将多个类放在一起有诸多好处。因为私有访问权限仅限于库层级,而不是类层级,所以在同一个库中是可以访问其它类中的私有属性和方法的。
Classes and mixins
Dart 是一个纯面向对象的语言,这意味这所有对象都是类的实例。但是另一方面,Dart 有可以像面向过程或者函数式编程一样拥有顶层方法和变量。
AVOID defining a one-member abstract class when a simple function will do.
当类中只有一个方法或者变量的时候,考虑使用顶层变量或者方法代替。
// good:
typedef Predicate = bool Function(E element);
// bad:
abstract class Predicate {
bool test(E element);
}
AVOID defining a class that contains only static members.
Java C# 等语言必须将方法、变量和常量定义在类之中,比如 Java 中,我们常常会用一个 Constants 类来保存全局使用到的一些静态常量。而且为了避免命名冲突,我们常常会使用类名区分相同名字的方法。Dart 中没有这样的限制,相反,Dart 使用 library 作为命名空间,并且在导入包的时候可以使用 as
/show
/hide
等关键字避免冲突的命名。
我们可以将静态方法或静态成员变量转换成顶层方法或常量,然后以合适的方式命名或者整理进同一个库中。当然,这个规则不一定必须要遵循,一些场合下可能还是使用类和静态成员变量更合适:
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
AVOID extending a class that isn’t intended to be subclassed.
有些类可能本意就不是为了被继承而设计的,所以尽量用合适的命名告诉库的使用者哪些类可以被继承而哪些类不行。
DO document if your class supports being extended.
同上,如果一个类可以被继承,至少用注释说明一些需要注意的地方。
AVOID implementing a class that isn’t intended to be an interface.
同样的,如果一个接口不应该被实现,而使用者却实现了这个接口,那么当未来库的作者对这个接口做任何改动,都会影响到使用者的原有的实现。
DO document if your class supports being used as an interface.
如果一个接口可以被实现,需要在文档注释中说明。
DO use mixin
to define a mixin type.
Dart 从版本 2.1.0 之后才添加了 mixin
关键字,在这之前,任何没有默认构造器、没有父类的类都可以被用做 mixin,这带来了一个问题,有些类可能并不适合用做 mixin,误用它们可能会带来一些问题。而使用了 mixin
关键字之后,mixin
类只能被用做 mixins,而不能用做其它用途。
AVOID mixing in a type that isn’t intended to be a mixin.
同上。
Constructors
Dart 中构造器是一类特殊的方法,用于创建类的实例,它的方法名和类名相同,没有返回值,并且除此之外还可以用 .
分割,后面加标识符,这类构造器叫具名构造器。
CONSIDER making your constructor const
if the class supports it.
如果类中的成员变量都是 final 的,而且构造器只是初始化了这些成员变量,那么可以用 const
修饰构造器。这样,使用该类的开发者就可以在需要使用常量的地方使用该类的对象了,比如在其它常量容器中,switch..case 中,默认参数值等等。
class Pet {
final String name;
const Pet(this.name);
}
class PetShop {
Pet pet;
// Error: The default value of an optional parameter must be constant.
// PetShop({this.pet = Pet('Cat')});
PetShop([this.pet = const Pet('Cat')]);
}
Members
PREFER making fields and top-level variables final
.
尽量将成员变量和顶层变量设为 final
的。
DO use getters for operations that conceptually access properties.
在 API 设计中,使用 getters 还是使用方法作为访问某个数据的方式是个微妙但重要的部分。Dart 中成员变量会自动生成 getters,所以访问成员变量和访问 getters,其本质是一样的。所以,通常来说,访问 getters 给人的感觉就像是访问成员变量,它意味着:
- 这种操作不需要接受参数,但是有返回值;
- 调用者只关心返回值;
- 这种操作并不会造成任何用户可见的 side effects;
- 这种操作具有幂等性,即无论调用多少次,结果相同;
- 返回的结果对象不会暴露原始对象的所有状态;
如果你的目标操作符合以上所有特点,则可以将这种操作定义成一个 getter 而不是方法。
DO use setters for operations that conceptually change properties.
与 getters 类似,使用 setters 也会遇到类似的困境,不同的是 setters 需要满足 filed-like 特质:
- 操作只接受一个参数,且不会产生返回值;
- 操作只会改变对象的某些状态;
- 操作具有等幂性;
DON’T define a setter without a corresponding getter.
一个可被修改的 setter 往往对应着一个供访问的 getter。
AVOID using runtime type tests to fake overloading.
Dart 中没有重载机制,有的人会定义一个方法,在方法中用 is
判断类型然后根据具体的类型做一些操作。这种操作虽然能达到目的,但是最好的做法还是使用一系列独立的方法,让用户根据不同的类型调用不同的方法。只有当一个对象具体类型不确定,需要在运行时根据不同的类型来调用特定的子类方法的时候,才可以把它们定义在一个方法内。
AVOID public late final
fields without initializers.
不像其它 final
成员变量,late final
的成员变量如果没有初始化器的话,Dart 会为它们生成 setters 函数,如果成员变量是 public 的,则意味着 setters 也是 public 的。我们将成员变量设置为 late 通常是希望稍后再去初始化它,比如在构造器中,而 late final
使得成员变量可能在被初始化之前就在外部被初始化了一遍。所以,最好的做法是:
- 不要使用
late
; - 使用
late
但是在声明时为其初始化; - 使用
late
但是将它标记为 private 的,同时为其提供一个 getter;
AVOID returning nullable Future
, Stream
, and collection types.
如果返回的是容器类型,尽量避免返回空的容器类型,一般使用返回空的容器或者直接返回 null。
AVOID returning this
from methods just to enable a fluent interface.
使用级联表达式而不是在方法中返回 this
实现链式调用。
Types
我们使用类型标注限制某部分代码能够使用什么样的值。类型通常出现在两个地方:变量声明处的类型标注 (type annotations) 和使用泛型时的类型参数 (generic invocations)。
类型标注通常就是我们所认为的静态类型,我们可以对变量、参数、属性、返回值使用类型标注,就像下面这个例子:
bool isEmpty(String parameter) { // 返回值和参数的类型标注
bool result = parameter.isEmpty; // 变量的类型标注
return result;
}
泛型调用时的类型参数可以是创建集合字面量,或者是调用泛型类的构造方法,或者是调用泛型方法。比如:
var lists = [1, 2]; // 创建集合时指定类型
lists.addAll(List.filled(3, 4)); // 调用泛型类的构造器并指定类型
lists.cast(); // 调用泛型方法
Type inference
类型标注是可选的,因为 Dart 会根据当前上下文推断出具体类型。当缺乏足够的信息推断出类型的时候,Dart 默认会使用 dynamic
类型。这种机制让类型推断看起来是安全的,但实际上使得类型检查完全失效了。
同时拥有类型推断和 dynamic
类型,使得我们在说代码是无类型的 (untyped) 时产生歧义,一个变量到底是动态类型还是没有写类型参数?所以,我们一般不说代码是无类型的,而是使用以下术语代替:
- 如果代码拥有类型标注,则它的类型即所标注的类型(废话)。
- 如果代码是推断类型,则说明 Dart 已经确定其类型。而如果类型推断失败,那么我们不把称它为 inferred。
- 如果代码是动态类型的,那么它的静态类型就是
dynamic
。这种情况下,代码既可以是主动被标注为dynamic
也可以是推断类型(使用var
关键字)。
换句话说,代码是标注类型还是推断类型,与它是否被标注为 dynamic
或者其它类型无关。
类型推断是个强有力的工具,可以帮助我们编码或阅读代码时跳过一些显而易见的部分(代码类型),让我们关注真正重要的代码逻辑。但是,显式的代码类型也同样重要,它可以帮助我们写出健壮、可维护的代码。
当然,类型推断也不是万能药,一些情况下还是应该使用类型标注。有时候类型推断提前确定了变量类型,但是该类型不是你想要的,比如变量在初始化后推断出了类型,但是你实际却想要使用另一个类型,这种情况下就只能使用显式的类型标注了。
理解上面这些概念之后,方便我们在解释接下来的这些原则时,不会造成歧义。首先,我们可以将大致的原则总结为以下几点:
- 当上下文不足以推断出类型的时候,请使用类型标注,即使你想要的是
dynamic
类型; - 不要标注局部变量或者泛型调用;
- 对于顶层变量和属性,尽量显式标注其类型,除非初始化器使得它们的类型显而易见;
DO type annotate variables without initializers.
如果没有变量没有立即被初始化,请使用类型标注。
DO type annotate fields and top-level variables if the type isn’t obvious.
如果变量类型不是显而易见的,也要使用类型标注。
显而易见包括以下这些情况:
- 字面量,如基本数据类型等
- 构造器中的参数
- 引用其它变量或者常量
- 简单的表达式,比如 isEmpty, ==, > 等等
- 工厂方法,比如 int.parse(), Future.wait() 等
另外,当你觉得类型标注可以使你的代码更清晰时,那就请使用类型标注。
When in doubt, add a type annotation.
DON’T redundantly type annotate initialized local variables.
有初始化器的局部变量不要使用类型标注。只有当你确定推断类型不是你想要的类型的时候才使用类型标注。
DO annotate return types on function declarations.
给方法返回值添加类型标注可以方便方法的调用者。当然,匿名方法就没必要了。
DO annotate parameter types on function declarations.
给方法的参数添加类型标注,同样很有必要,可以帮助方法的调用者确定参数的边界。
需要注意的是,Dart 不会对可选的参数做类型推断,来源:
void sayRepeatedly(String message, {int count = 2}) {
for (var i = 0; i < count; i++) {
print(message);
}
}
DON’T annotate inferred parameter types on function expressions.
Dart 通常可以根据上下文确定匿名方法接收的参数是什么,所以匿名方法一般不需要添加类型标注。
var names = people.map((person) => person.name);
DON’T type annotate initializing formals.
之前说过构造器中使用 this.
给属性赋值的形式叫做 initializing formals
,这种情况下也不要使用类型标注。
class Point {
double x, y;
Point(this.x, this.y);
}
DO write type arguments on generic invocations that aren’t inferred.
一些情况下,泛型的类型无法被确定,比如空的集合,所以我们需要为它们标注类型。
var playerScores = {};
final events = StreamController();
// 对于成员变量来说,如果类型同样无法推断出,则需要在声明处标注类型
class Downloader {
final Completer response = Completer();
}
DON’T write type arguments on generic invocations that are inferred.
如果泛型类的类型已经推断出来,就不要在写类型了。
class Downloader {
final Completer response = Completer(); // 错误示例
}
AVOID writing incomplete generic types.
也就是不要使用 raw 泛型。
// bad:
List numbers = [1, 2, 3];
var completer = Completer
DO annotate with dynamic
instead of letting inference fail.
显式标明 dynamic
永远要比不写类型标注要好。
// good:
dynamic mergeJson(dynamic original, dynamic changes) => ...
// bad:
mergeJson(original, changes) => ...
当然,有些情况下,Dart 也能推断出 dyanmic
类型的:
Map readJson() => ...
void printUsers() {
var json = readJson();
var users = json['users'];
}
PREFER signatures in function type annotations.
默认的 Function 允许任何类型的返回值和参数,如果不带签名使用,在有些情况下会导致错误。
// good:
bool isValid(String value, bool Function(String) test) => ...
// bad:
bool isValid(String value, Function test) => ...
DON’T specify a return type for a setter.
Dart 中 setters 只会返回 void,所以不需要写返回值。
DON’T use the legacy typedef syntax.
又是一个历史遗留问题,Dart 中有两种方式定义 typedef,推荐使用新的写法。
// bad:
typedef int Comparison(T a, T b);
// good:
typedef Comparison = int Function(T a, T b);
PREFER inline function types over typedefs.
Dart 2 开始支持 inline function,我们可以直接定义方法作为类型签名。
class FilteredObservable {
final bool Function(Event) _predicate;
final List _observers;
FilteredObservable(this._predicate, this._observers);
void Function(Event)? notify(Event event) {
if (!_predicate(event)) return null;
void Function(Event)? last;
for (var observer in _observers) {
observer(event);
last = observer;
}
return last;
}
}
如果方法很复杂或者多次使用的情况下,推荐使用 typedef 代替。
PREFER using function type syntax for parameters.
就像方法可以作为类型标注一样,方法也可以作为参数,并且有特殊的语法支持:
// 函数形式的参数:返回值 Function(参数类型) 参数名
Iterable where(bool Function(T) predicate) => ...
AVOID using dynamic
unless you want to disable static checking.
Dart 中 dynamic
是一个非常特殊的类型,它的作用和 Object?
类似,都允许任何对象,包括 null,但是 dynamic
还有额外的功能,那就是默认允许任何操作,包括对任何成员的访问,无论这种访问是否有效或者合法,Dart 不会在编译期对其进行检查,如果有异常只会在运行期才会被抛出。除非你确认想要这种效果,否则还是使用 Obejct?
或者 Object
代替 dynamic
,然后用 is
对类型进行进行检查和类型提升。
/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(Object arg) {
if (arg is bool) return arg;
if (arg is String) return arg.toLowerCase() == 'true';
throw ArgumentError('Cannot convert $arg to a bool.');
}
DO use Future
as the return type of asynchronous members that do not produce values.
如果异步方法没有值需要返回,请使用 Future
AVOID using FutureOr
as a return type.
如果方法接受 FutureOr
作为参数,那么它可以接收 int 或者 Future
作为参数,这样可以方便调用者用 Future 包装 int 后再调用你的方法。但是,如果你返回 FutureOr
,方法的调用者就需要检查返回值到底是 int 还是 Future
。推荐的做法是直接返回 Future
,这样调用者可以直接使用 await
获取异步结果值。
Future triple(FutureOr value) async => (await value) * 3;
Parameters
AVOID positional boolean parameters.
可选布尔值不但容易让调用着分不清参数的含义,而且容易出错。
// bad:
new ListBox(false, true, true);
// good:
ListBox(scroll: true, showScrollbars: true);
AVOID optional positional parameters if the user may want to omit earlier parameters.
对于可选位置参数,调用者可能省略中间或者后面部分,尽量把关键部分写在前面,或者使用具名位置参数。
// 调用方可能省略一部分可选位置参数,因此,最重要的写在前面
String.fromCharCodes(Iterable charCodes, [int start = 0, int? end]);
// 使用具名位置参数就没有这个烦恼了
Duration(
{int days = 0,
int hours = 0,
int minutes = 0,
int seconds = 0,
int milliseconds = 0,
int microseconds = 0});
AVOID mandatory parameters that accept a special “no argument” value.
不要强制用户传 null,使用可选参数代替。
// bad:
var rest = string.substring(start, null);
// good:
var rest = string.substring(start);
DO use inclusive start and exclusive end parameters to accept a range.
当方法接收的参数用数字下标表示范围时,尽量采用前闭后开的习俗,包括开头下标但是不包括结尾的下标。
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
Equality
DO override hashCode
if you override ==
.
这是约定俗成的。两个对象相同则说明它们的哈希值一致,否则类似于 Map 等基于哈希值的集合就无法使用了。
DO make your ==
operator obey the mathematical rules of equality.
- 自反性:
a == a
永远返回 true; - 对称性:
a == b
为 true 时b == a
也必定为 true; - 传递性:
a == b
和b == c
都为 true,则a == c
也为 true;
AVOID defining custom equality for mutable classes.
如果是可变的对象,比如拥有可变属性的对象,他们的哈希值会随着属性的变化而变化,但是大多数基于哈希的集合没有考虑到这一点,因此,最好不要自定义可变对象的相等性。
DON’T make the parameter to ==
nullable.
Dart 语言中 null 只能等于 null,因此,使用 ==
比较对象时,右边的对象不能是 null。
class Person {
final String name;
// bad:
bool operator ==(Object? other) =>
other != null && other is Person && name == other.name;
// good:
bool operator ==(Object other) => other is Person && name == other.name;
}