一个项目中会有多名开发中同时进行code,为了项目的健壮和可维护性我们需要制定一套代码规范,约束开发者的coding.

Dart官方推荐一套代码规范.这套规范适用于其他语言的项目代码规范.

概述

规范分为四个部分:

  1. 风格指南 – 这定义了布局和组织代码的规则, dartfmt 的实现使用同样的规则。
  2. 注释指南 – 这会告诉你关于如何编写注释文档的一切内容。 包括文档注释,常规的普通代码注释。
  3. 使用指南 – 这将教你如何充分利用语言功能来实现功能。 例如语句和表达式相关的内容,则会在这里介绍。
  4. 设计指南 – 这是一份宽松的指南,但是覆盖范围最广。 这里涵盖了如何为库设计一致的、可用的 API。例如类型签名或声明相关内容, 则会在这里介绍。

每条准则都以下面其中的一个词作为开头:

  • 准则所描述的内容应该始终被遵守。 不应该以任何的理由来偏离违背这些准则。
  • 不要 准则所描述的内容是相反的: 描述的几乎从来不是一个好注意。 幸运的是,我们不会像其他语言有那么多这样的准则,因为我们没有太多的历史包袱。
  • 推荐 准则所描述的内容应该被遵守。 但是在有些情况下,可能有更好的或者更合理的做法。 请确保在你完全理解准则的情况下,再忽视这些准则。
  • 避免 准则与 “推荐” 准则相反: 显然,这些事不应该做,但不排除在极少数场合下有充分的理由可以使用。
  • 考虑 准则所描述的内容可以遵守,也可以不遵守。 取决于具体的情况、先前的做法以及自己的偏好。

为了使指南保持简洁, 我们使用一些简写术语来指代不同的 Dart 结构。

  • 库成员 是一个顶级字段,getter 方法,setter 方法,或者函数, 基本上,任何顶级的东西都不会是一种类型。
  • 类成员 是一个类内部声明的构造函数,字段,getter 方法,setter 方法,函数,或者操作符。 类成员可以是实例的或者静态的,抽象的或者具体的。
  • 成员 是一个库成员或者是类成员。
  • 变量 通常是指顶级变量,参数和局部变量。 它不包括静态或实例的字段。
  • 类型 是任意命名类型的声明:一个类、 typedef、或者 enum。
  • 属性 是一个顶级变量,getter 方法(在一个类或者顶级实例或静态类中), setter(同getter),或者字段(示例或静态类)。 大致上任何“字段式”命名构造。

风格

标识符

在 Dart 中标识符有三种类型。

  • UpperCamelCase 每个单词的首字母都大写,包含第一个单词。
  • lowerCamelCase 每个单词的首字母都大写,除了第一个单词, 第一个单词首字母小写,即使是缩略词。
  • lowercase_with_underscores 只是用小写字母单词,即使是缩略词, 并且单词之间使用 _ 连接。

要使用 UpperCamelCase 风格命名类型

Classes(类名)、 enums(枚举类型)、 typedefs(类型定义)、 以及 type parameters(类型参数)应该把每个单词的首字母都大写(包含第一个单词), 不使用分隔符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SliderMenu { ... }

class HttpRequest { ... }

typedef Predicate = bool Function<T>(T value);

class Foo {
const Foo([arg]);
}

@Foo(anArg)
class A { ... }

@Foo()
class B { ... }

要在文件夹源文件中使用 lowercase_with_underscores 方式命名

1
2
3
4
library peg_parser.source_scanner;

import 'file_system.dart';
import 'slider_menu.dart';

要使用 lowercase_with_underscores 风格命名导入的前缀

1
2
3
4
import 'dart:math' as math;
import 'package:angular_components/angular_components'
as angular_components;
import 'package:js/js.dart' as js;

要使用 lowerCamelCase 风格来命名其他的标识符

类成员、顶级定义、变量、参数以及命名参数等 除了第一个单词,每个单词首字母都应大写,并且不使用分隔符

1
2
3
4
5
6
7
var item;

HttpRequest httpRequest;

void align(bool clearItems) {
// ...
}

推荐使用 lowerCamelCase 来命名常量

在新的代码中,使用 lowerCamelCase 来命名常量,包括枚举的值

1
2
3
4
5
6
7
const pi = 3.14;
const defaultTimeout = 1000;
final urlScheme = RegExp('^([a-z]+):');

class Dice {
static final numberGenerator = Random();
}

要把超过两个字母的首字母大写缩略词和缩写词当做一般单词来对待

首字母大写缩略词比较难阅读, 特别是多个缩略词连载一起的时候会引起歧义。 例如,一个以 HTTPSFTP 开头的名字, 没有办法判断它是指 HTTPS FTP 还是 HTTP SFTP 。

为了避免上面的情况,缩略词和缩写词要像普通单词一样首字母大写, 两个字母的单词除外。 (像 ID 和 Mr. 这样的双字母缩写词仍然像一般单词一样首字母大写。)

1
2
3
4
5
6
HttpConnectionInfo
uiHandler
IOStream
HttpRequest
Id
DB

不要 使用前缀字母

顺序

为了使文件前面部分保持整洁,我们规定了关键字出现顺序的规则。 每个“部分”应该使用空行分割。

把 “dart:” 导入语句放到其他导入语句之前

1
2
3
4
5
import 'dart:async';
import 'dart:html';

import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

把 “package:” 导入语句放到项目相关导入语句之前。

1
2
3
4
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'util.dart';

推荐 把外部扩展 “package:” 导入语句放到其他语句之前

如果你使用了多个 “package:” 导入语句来导入自己的包以及其他外部扩展包, 推荐将自己的包分开放到一个额外的部分

1
2
3
4
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'package:my_package/util.dart';

把导出(export)语句作为一个单独的部分放到所有导入语句之后

1
2
3
4
import 'src/error.dart';
import 'src/foo_bar.dart';

export 'src/error.dart';

按照字母顺序来排序每个部分中的语句

1
2
3
4
5
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';

import 'foo.dart';
import 'foo/foo.dart';

格式化

和其他大部分语言一样, Dart 忽略空格。但是,不会。 具有一致的空格风格有助于帮助我们能够用编译器相同的方式理解代码。

要使用 dartfmt 格式化你的代码

避免单行超过 80 个字符

注释

要像句子一样来格式化注释

1
2
// Not if there is nothing before it.
if (_chunks.isEmpty) return false;

不要 使用块注释作用作解释说明

1
2
3
4
greet(name) {
// Assume we have a valid name.
print('Hi, $name!');
}

要使用 /// 文档注释来注释成员和类型

要在文档注释开头有一个单句总结

考虑在文档注释中添加示例代码

要使用方括号在文档注释中引用作用域内的标识符

使用

要在 part of 中使用字符串

很多 Dart 开发者会避免直接使用 part 。他们发现当库仅有一个文件的时候很容易读懂代码。 如果你确实要使用 part 将库的一部分拆分为另一个文件,则 Dart 要求另一个文件指示它所属库的路径。 由于遗留原因, Dart 允许 part of 指令使用它所属的库的名称。 这使得工具很难直接查找到这个文件对应主库文件,使得库和文件之间的关系模糊不清。

推荐的现代语法是使用 URI 字符串直接指向库文件。 首选的现代语法是使用直接指向库文件的URI字符串,URI 的使用和其他指令中一样。 如果你有一些库,my_library.dart,其中包含:

1
2
3
library my_library;

part "some/other/file.dart";

从库中拆分的文件应该如下所示:

1
part of "../../my_library.dart";

不要 导入 package 中 src 目录下的库

lib 下的 src 目录被指定为 package 自己实现的私有库。 基于包维护者对版本的考虑,package 使用了这种约定。 在不破坏 package 的情况下,维护者可以自由地对 src目录下的代码进行修改。

建议使用相对路径在导入你自己 package 中的 lib 目录

要使用临近字符字的方式连接字面量字符串

避免在字符串插值中使用不必要的大括号

1
2
3
'Hi, $name!'
"Wear your wildest $decade's outfit."
'Wear your wildest ${decade}s outfit.'

要 尽可能的使用集合字面量

Dart 集合中原生支持了四种类型:list, map, queue, 和 set

1
2
3
4
5
var points = [];
var addresses = {};

var points = <Point>[];
var addresses = <String, Address>{};

不要使用 .length 来判断一个集合是否为空

Dart 提供了更加高效率和易用的 getter 函数:.isEmpty.isNotEmpty

1
2
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');

考虑使用高阶(higher-order)函数来转换集合数据

如果你有一个集合并且想要修改里面的内容转换为另外一个集合, 使用 .map().where() 以及 Iterable 提供的其他函数会让代码更加简洁

使用这些函数替代 for 循环会让代码更加可以表述你的意图, 生成一个新的集合系列并不具有副作用

1
2
3
var aquaticNames = animals
.where((animal) => animal.isAquatic)
.map((animal) => animal.name);

避免在 Iterable.forEach() 中使用字面量函数

forEach() 函数在 JavaScript 中被广泛使用, 这因为内置的 for-in 循环通常不能达到你想要的效果。 在Dart中,如果要对序列进行迭代,惯用的方式是使用循环。

1
2
3
for (var person in people) {
...
}

不要使用 List.from() 除非想修改结果的类型

给定一个可迭代的对象,有两种常见方式来生成一个包含相同元素的 list

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

1
2
3
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

要使用 whereType() 按类型过滤集合

假设你有一个 list 里面包含了多种类型的对象, 但是你指向从它里面获取整型类型的数据。 那么你可以像下面这样使用 where()

1
2
3
4
5
6
7
8
9
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()]where-type方法:

1
2
var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType<int>();

避免使用 cast()

避免使用 cast() 来“改变”集合中元素的类型

推荐使用下面的方式来替代:

  • 用恰当的类型创建集合。 修改集合被首次创建时的代码, 为集合提供有一个恰当的类型。

  • 在访问元素时进行 cast 操作。 如果要立即对集合进行迭代, 在迭代内部 cast 每个元素。

  • 逼不得已进行 cast,请使用 List.from() 。 如果最终你会使用到集合中的大部分元素, 并且不需要对象还原到原始的对象类型,使用 List.from() 来转换它。

    cast() 方法返回一个惰性集合(lazy collection),每个操作都会对元素进行检查。 如果只对少数元素执行少量操作,那么这种惰性方式就非常合适。 但在许多情况下,惰性验证和包裹(wrapping)所产生的开销已经超过了它们所带来的好处。

使用函数声明的方式为函数绑定名称

1
2
3
4
5
void main() {
localFunction() {
...
}
}

不要 为字段创建不必要的 getter 和 setter 方法

在 Java 和 C# 中,通常情况下会将所有的字段隐藏到 getter 和 setter 方法中(在 C# 中被称为属性), 即使实现中仅仅是指向这些字段。在这种方式下,即使你在这些成员上做多少的事情,你也不需要直接访问它们。 这是因为,在 Java 中,调用 getter 方法和直接访问字段是不同的。 在 C# 中,访问属性与访问字段不是二进制兼容的。

Dart 不存在这个限制。字段和 getter/setter 是完全无法区分的。 你可以在类中公开一个字段,然后将其包装在 getter 和 setter 中, 而不会影响任何使用该字段的代码。

推荐 使用 final 关键字来创建只读属性。

考虑 对简单成员使用 =>

除了使用 => 可以用作函数表达式以外, Dart 还允许使用它来定义成员。 这种风格非常适合,仅进行计算并返回结果的简单成员。

1
2
3
4
5
6
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)}';

设计

命名

使用一致的术语

在你的代码中,同样的东西要使用同样的名字。 如果之前已经存在的 API 之外命名,并且用户已经熟知, 那么请继续使用这个命名。

1
2
3
4
5
pageCount         // A field.
updatePageCount() // Consistent with pageCount.
toSomething() // Consistent with Iterable's toList().
asSomething() // Consistent with List's asMap().
Point // A familiar concept.

推荐 把最具描述性的名词放到最后

最后一个词应该是最具描述性的东西。 你可以在其前面添加其他单词,例如形容词,以进一步描述该事物。

1
2
3
4
pageCount             // A count (of pages).
ConversionSink // A sink for doing conversions.
ChunkedConversionSink // A ConversionSink that's chunked.
CssFontFaceRule // A rule for font faces in CSS.

考虑 尽量让代码看起来像普通的句子

当你不知道如何命名 API 的时候, 使用你的 API 编写些代码,试着让代码看起来像普通的句子

1
2
3
4
5
6
7
8
// "If errors is empty..."
if (errors.isEmpty) ...

// "Hey, subscription, cancel!"
subscription.cancel();

// "Get the monsters where the monster has claws."
monsters.where((monster) => monster.hasClaws);

推荐 使用名词短语来命名不是布尔类型的变量和属性

1
2
3
list.length
context.lineWidth
quest.rampagingSwampBeast

推荐 使用非命令式动词短语命名布尔类型的变量和属性

布尔名称通常用在控制语句中当做条件, 因此你要应该让这个名字在控制语句中读起来语感很好

1
2
3
4
5
6
7
8
9
if (window.closeable) ...  // Adjective.
if (window.canClose) ... // Verb.

isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup

考虑 省略命名布尔参数的动词

对于命名布尔参数, 没有动词的名称通常看起来更加舒服。

1
2
3
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);

考虑 为布尔属性或变量取“肯定”含义的名字

大多数布尔值名称具有概念形式上的“肯定”和“否定”, 前者感觉更现实基本描述,后者是对基本描述的否定,例如: “open” 和 “closed”, “enabled” 和 “disabled”,等等。 通常后者的名称字面上有个前缀,用来否定前者: “visible” 和 “in-visible”, “connected” 和 “dis-connected”, “zero” 和 “non-zero”。

当选择 true 代表两种情况中的其中一种情况 在布尔的两种情况中,当选择 true 代表其中一种情况, 或使用这种情况作为属性名称时,更倾向使用“肯定”或基本描述的方式。 布尔成员通常嵌套在逻辑表达式中,包括否定运算符。 如果属性本身读起来想是个“否定”的, 这将让读者耗费更多精力去阅读双重否定及理解代码的含义。

1
2
3
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}

推荐 使用命令式动词短语来命名带有副作用的函数或者方法

函数通常返回一个结果给调用者,并且执行一些任务或者带有副作用。 在像 Dart 这种命令式语言中,调用函数通常为了实现其副作用: 可能改变了对象的内部状态、 产生一些输出内容、或者和外部世界沟通等。

1
2
3
list.add("element");
queue.removeFirst();
window.refresh();

考虑 使用名词短语或者非命令式动词短语命名返回数据为主要功能的方法或者函数

虽然这些函数可能也有副作用,但是其主要目的是返回一个数据给调用者。 如果该函数无需参数通常应该是一个 getter 。 有时候获取一个属性则需要一些参数,比如,elementAt() 从集合中返回一个数据,但是需要一个指定返回那个数据的参数。

1
2
3
var element = list.elementAt(3);
var first = list.firstWhere(test);
var char = string.codeUnitAt(4);

考虑 使用命令式动词短语命名一个函数或方法,若果你希望它的执行能被重视

当一个成员产生的结果没有额外的影响,它通常应该使用一个 getter 或者一个名词短语描述来命名,用于描述它返回的结果。 但是,有时候执行产生的结果很重要。 它可能容易导致运行时故障,或者使用重量级的资源(例如,网络或文件 I/O)。 在这种情况下,你希望调用者考虑成员在进行的工作, 这时,为成员提供描述该工作的动词短语。

1
2
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();

避免 在方法命名中使用 get 开头

在大多数情况下,getter 方法名称中应该移除 get 。 例如,定义一个名为 breakfastOrder 的 getter 方法, 来替代名为 getBreakfastOrder() 的方法。

即使成员因为需要传入参数或者 getter 不适用, 而需要通过方法来实现,也应该避免使用 get 开头。 与之前的准则一样:

  • 如果调用者主要关心的是方法的返回值,只需删除 get 并使用名词短语命名, 如 breakfastOrder()
  • 如果调用者关心的是正在完成的工作,请使用动名词短语命名, 这种情况下应该选择一个更能准确描述工作的动名词,而不是使用 get 命名, 如 createdownloadfetchcalculaterequestaggregate,等等。

推荐 使用 to___() 来命名把对象的状态转换到一个新的对象的函数

推荐 使用 as___() 来命名把原来对象转换为另外一种表现形式的函数

避免 在方法或者函数名称中描述参数

1
2
3
list.add(element);
map.remove(key);
/// 调用代码的时候可以看到参数,所以无需再次显示参数了

在命名参数时,遵循现有的助记符约定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// E 用于集合中的 元素 类型
class IterableBase<E> {}
class List<E> {}
class HashSet<E> {}
class RedBlackTree<E> {}

//K 和 V 分别用于关联集合中的 key 和 value 类型
class Map<K, V> {}
class Multimap<K, V> {}
class MapEntry<K, V> {}
// R 用于函数或类方法的 返回值 类型
abstract class ExpressionVisitor<R> {
R visitBinary(BinaryExpression node);
R visitLiteral(LiteralExpression node);
R visitUnary(UnaryExpression node);
}


/// 除此以外,对于具有单个类型参数的泛型,如果助记符能在周围类型中明显表达泛型含义, 请使用T,S 和 U 。 /// 这里允许多个字母嵌套且不会与周围命名产生歧义。例如:
class Future<T> {
Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
}