目录

Effective Dart: 设计

下面给出的准则用于指导为库编写一致的、可用的 API。

命名

命名是编写可读,可维护代码的重要部分。 以下最佳实践可帮助你实现这个目标。

使用一致的术语。

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

pageCount         // A field.
updatePageCount() // Consistent with pageCount.
toSomething()     // Consistent with Iterable's toList().
asSomething()     // Consistent with List's asMap().
Point             // A familiar concept.
renumberPages()      // Confusingly different from pageCount.
convertToSomething() // Inconsistent with toX() precedent.
wrappedAsSomething() // Inconsistent with asX() precedent.
Cartesian            // Unfamiliar to most users.

总的目的是充分利用用户已经知道的内容。 这里包括他们所了解的问题领域,所熟悉的核心库,以及你自己 API 那部分。 基于以上这些内容,他们在使用之前,不需要学习大量的新知识。

避免 缩写。

只使用广为人知的缩写,对于特有领域的缩写,请避免使用。 如果要使用,请 正确的指定首字母大小写

pageCount
buildRectangles
IOStream
HttpRequest
numPages    // "num" is an abbreviation of number(of)
buildRects
InputOutputStream
HypertextTransferProtocolRequest

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

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

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.
numPages                  // Not a collection of pages.
CanvasRenderingContext2D  // Not a "2D".
RuleFontFaceCss           // Not a CSS.

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

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

// "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);
// Telling errors to empty itself, or asking if it is?
if (errors.empty) ...

// Toggle what? To what?
subscription.toggle();

// Filter the monsters with claws *out* or include *only* those?
monsters.filter((monster) => monster.hasClaws);

尝试着使用你自己的 API,并且阅读写出来的代码,可以帮助你为 API 命名,但是不要过于冗余。 添加文章和其他词性以强制名字读起来就像语法正确的句子一样,是没用的。

if (theCollectionOfErrors.isEmpty) ...

monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);

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

读者关注属性是什么。 如果用户更关心如何确定一个属性,则很可能应该是一个使用动词短语命名函数。

list.length
context.lineWidth
quest.rampagingSwampBeast
list.deleteItems

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

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

if (window.closeable) ...  // Adjective.
if (window.canClose) ...   // Verb.

好的名字往往以某一种动词作为开头:

  • “to be” 形式: isEnabledwasShownwillFire。 就目前来看,这些时做常见的。

  • 一个 [辅助动词][]: hasElementscanCloseshouldConsumemustSave

  • 一个主动动词: ignoresInputwroteFile。 因为经常引起歧义,所以这种形式比较少见。 loggedResult 是一个不好的命名,因为它的意思可能是: “whether or not a result was logged” 或者 “the result that was logged”。 closingConnection 的意思可能是: “whether the connection is closing” 或者 “the connection that is closing”。 只有 当名字可以预期的时候才使用主动动词。

可以使用命令式动词来区分布尔变量名字和函数名字。 一个布尔变量的名字不应该看起来像一个命令,告诉这个对象做什么事情。 原因在于访问一个变量的属性并没有修改对象的状态。 (如果这个属性确实修改了对象的状态,则它应该是一个函数。)

isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
empty         // Adjective or verb?
withElements  // Sounds like it might hold elements.
closeable     // Sounds like an interface.
              // "canClose" reads better as a sentence.
closingWindow // Returns a bool or a window?
showPopup     // Sounds like it shows the popup.

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

提炼于上一条规则。对于命名布尔参数, 没有动词的名称通常看起来更加舒服。

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

if (socket.isConnected && database.hasData) {
  socket.write(database.read());
}
if (!socket.isDisconnected && !database.isEmpty) {
  socket.write(database.read());
}

上面规则中有一个例外,就是“否定”用户绝大多数用到的形式是。 选择“肯定”方式,将会迫使在他们到处使用 ! 对属性进行取反操作。 这样相反,属性应该使用“否定”形式进行命名。

对于一些属性,没有明显的“肯定”形式。 文档已经刷新 “saved” 到磁盘,或者 “un-changed”? 文档还未属性 “un-saved” 到磁盘,或者 “changed”? 在模棱两可的情况下,倾向于选择不太可能被用户否定或较短的名字。

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

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

这种类型的成员应该使用命令式动词短语来命名,强调 该成员所执行的任务。

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

这样,调用的方法读起来会让人觉得是一个执行命令。

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

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

语法上看这是一个函数,其实严格来说其返回的是集合中的一个属性, 应该使用一个能够表示该函数返回的是什么的词语来命名。

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

这条规则比前一条要宽松一些。有时候一些 函数没有副作用,但仍然使用一个动词短语来命名,例如: list.take() 或者 string.split()

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

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

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

但请注意,此准则比前两个更宽松。操作执行工作的实现细节通常与调用这无关, 并且性能和健壮性是随时间经常改变的。 大多数情况下,根据成员为调用者做了“什么”来命名,而不是“如何”做。

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

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

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

  • 如果调用者主要关心的是方法的返回值,只需删除 get 并使用名词短语命名, 如 breakfastOrder()

  • 如果调用者关心的是正在完成的工作,请使用动名词短语命名, 这种情况下应该选择一个更能准确描述工作的动名词,而不是使用 get 命名, 如 createdownloadfetchcalculaterequestaggregate,等等。

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

Linter rule: use_to_and_as_if_applicable

一个转换函数返回一个新的对象,里面包含一些原对象的状态,但通常新对象的形式或表现方式与原对象不同。 核心库有一个约定,这些类型结果的方法名应该以 to 作为开头。

如果要定义一个转换函数,遵循该约定是非常有益的。

list.toSet();
stackTrace.toString();
dateTime.toLocal();

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

Linter rule: use_to_and_as_if_applicable

转换函数提供的是“快照功能”。返回的对象有自己的数据副本, 修改原来对象的数据不会改变返回的对象中的数据。 另外一种函数返回的是同一份数据的另外一种表现形式,返回的是一个新的对象, 但是其内部引用的数据和原来对象引用的数据一样。 修改原来对象中的数据,新返回的对象中的数据也一起被修改。

这种函数在核心库中被命名为 as___()

var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();

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

在调用代码的时候可以看到参数,所以无需再次显示参数了。

list.add(element);
map.remove(key);
list.addElement(element)
map.removeKey(key)

但是,对于具有多个类似的函数的时候,使用参数名字可以消除歧义, 这个时候应该带有参数名字。

map.containsKey(key);
map.containsValue(value);

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

单字母命名没有直接的启发性,但是几乎所有通用类型都使用时情况就不一样了。 幸运的是,它们大多数以一致的助记方式在使用,这些约定如下:

  • E 用于集合中的 元素 类型:

    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • KV 分别用于关联集合中的 keyvalue 类型:

    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • R 用于函数或类方法的 返回值 类型。 这种情况并不常见, 但有时会出现在typedef中,或实现访问者模式的类中:

    abstract class ExpressionVisitor<R> {
      R visitBinary(BinaryExpression node);
      R visitLiteral(LiteralExpression node);
      R visitUnary(UnaryExpression node);
    }
  • 除此以外,对于具有单个类型参数的泛型,如果助记符能在周围类型中明显表达泛型含义, 请使用TSU 。 这里允许多个字母嵌套且不会与周围命名产生歧义。例如:

    class Future<T> {
      Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
    }

    这里,通常 then<S>() 方法使用 S 避免 Future<T> 中的 T 产生歧义。

如果上述情况都不合适,则可以使用另一个单字母助记符名称或描述性的名称:

class Graph<N, E> {
  final List<N> nodes = [];
  final List<E> edges = [];
}

class Graph<Node, Edge> {
  final List<Node> nodes = [];
  final List<Edge> edges = [];
}

在实践中,以上的约定涵盖了大多数参数类型。

以 ( _ ) 开头的成员只能在其库的内部被访问,是库的私有成员。 这是 Dart 语言的内置特性,不仅仅是惯例。

推荐 使用私有声明。

库中的公开声明—顶级定义或者在类中定义—是一种信号, 表示其他库可以并应该访问这些成员。 同时公开声明也是一种你的库需要实现的契约, 当使用这些成员的时候,应该实现其宣称的功能。

如果某个成员你不希望公开,则在成员名字之前添加一个 _ 即可。 减少公开的接口让你的库更容易维护,也让用户更加容易掌握你的库如何使用。

另外,分析工具还可以分析出没有用到的私有成员定义,然后告诉你可以删除这些无用的代码。 私有成员第三方代码无法调用而你自己在库中也没有使用,所以是无用的代码。

考虑 声明多个类在一个库中。

一些其他语言,比如 Java。将文件结构和类结构进行捆绑&mdash:每个文件仅能定义一个顶级类。 Dart 没有这样的限制。库与类是相互独立的。如果多个类,顶级变量,以及函数,他们再逻辑上 归为同一类,那么将他们包含到单一的库中,这样做是非常棒的。

将多个类组织到一个库中,就可以使用一些有用的模式。因为在 Dart 中私有特性是在库级别上有效, 而不是在类级别,基于这个模式你可以定义类似于 C++ 中的 “friend” 类。所有定义在同一个库中 的类可以互相访问彼此的私有成员,但库以外的代码无法发访问。

当然,该规则并不意味着你应该将你所有的类组织到一个庞大单一的库中,规则只是说允许你将多 个类组织到一个库中。

Dart是一种 “纯粹的” 面向对象语言,因为所有对象都是类的实例。但是 Dart 并没有要求所有代码都 定义到类中— 类似在面向过程或函数的语言,你可以在 Dart 中定义顶级变量,常量,以及函数。

避免 避免为了使用一个简单的函数而去定义一个单一成员的抽象类

Linter rule: one_member_abstracts

和 Java 不同,Dart 拥有一等公民的函数,闭包,以及它们简洁的使用语法。如果你仅仅是需要一个 类似于回调的功能,那么使用函数即可。 例如如果你正在定义一个类,并且它仅拥有一个毫无意义名称的 抽象成员,如 callinvoke ,那么这时你很可能只是需要一个函数。

typedef Predicate<E> = bool Function(E element);
abstract class Predicate<E> {
  bool test(E element);
}

避免 定义仅包含静态成员的类。

Linter rule: avoid_classes_with_only_static_members

在 Java 和 C# 中,所有的定义必须要在类中。所有常常会看到一些这样的类,这些 类中仅仅放置了些静态成员。其他类仅用于命名空间—一种为一堆成员提供共享 前缀将它们相互关联或避免名称冲突的方法。

Dart 拥有一等公民的函数,变量,以及常量,所以你不需要通过类来定义这些东西。 如果你想要的是一个命名空间,那么使用库即可。库支持导入前缀和显示/隐藏组合器。 这些功能强大的工具可让代码的开发者以最适合他们的方式处理名称冲突。

如果函数或变量在逻辑上与类无关,那么应该将其置于顶层。如果担心名称冲突, 那么请为其指定更精确的名称,或将其移动到可以使用前缀导入的单独库中。

DateTime mostRecent(List<DateTime> dates) {
  return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}

const _favoriteMammal = 'weasel';
class DateUtils {
  static DateTime mostRecent(List<DateTime> dates) {
    return dates.reduce((a, b) => a.isAfter(b) ? a : b);
  }
}

class _Favorites {
  static const mammal = 'weasel';
}

通常在 Dart 中,类定义了一类对象。一个类型,如果类型从来没有被初始化, 那么这是另一种的代码气息。

当然,这并不是一条硬性规则。对于常量和类似枚举的类型,将它们组合在一个类 中看起来也是很自然。

class Color {
  static const red = '#f00';
  static const green = '#0f0';
  static const blue = '#00f';
  static const black = '#000';
  static const white = '#fff';
}

避免 集成一个不期望被集成的类。

如果一个类的构造函数从生成构造函数被更改为工厂构造函数,则调用该构造函数的任何子类构造函数都 将失败。 此外,如果一个类改变了它在 this 上调用的自己的方法,那么覆盖这些方法并期望他们在 某些点被调用的子类再调用时会失败。

以上两种情况都意味着一个类需要考虑是否要允许被子类化。这种情况可以通过文档注释来沟通,或者为类 提供一个显示命名,如 IterableBase。如果该类的作者不这样做,最好假设你能够继承这个类。 否则,后续对它的修改可能会破坏你的代码。

把能够继承的说明添加到文档中,如果这个类可以继承。

该规则是上条规则的结果。如果允许你的类被子类化,请在文档中说明情况。使用 Base 作为类名的后缀, 或者在类的注释文档中注明。

避免 去实现一个不期望成为接口的类(该类不想作为接口被实现)。

隐式接口是Dart中的一个强大工具,当一个类中可以很容易的推断出一些已经约定的有特征的实现时, 隐式接口可以避免重复定义这个类的约定。

但是通过类的隐式接口实现的新类,新类会与这个类产生非常紧密的耦合。也就是说,对于接口类的 任何修改,你实现的新类都会被破坏。例如,向类中添加新成员通常是安全,不会产生破坏性的改变。 但是如果你实现了这个类的接口,那么现在你的类会产生一个静态错误,因为它缺少了新方法的实现。

库的维护人员需要能够在不破坏用户代码的情况下迭代现有的累。如果把每个类都看待成是暴露给用户 的接口,用户可以自由的实现,这时修改这些类将变得非常困难。反过来,这个困难将导致你的库 迭代缓慢,从而无法适应新的需求。

为了给你的类的开发人员提供更多的余地,避免实现隐式接口,除非那些类明确需要实现。否则, 你可能会引入开发者没有预料到的耦合情况,这样可能会在没有意识到的情况下破坏你的代码。

对支持接口的类在文档注明

如果你的类可以被用作接口,那么将这个情况注明到类的文档中。

避免 去 mixin 一个不期望被 mixin 的类

如果在一个类中定义了一个之前从来没有被定义过的构造函数,那么这会破坏已被混入的其他类。 在类中,这样看似无害的变化,并且对 mixin 的限制和并不为其他人说知。作者可能会添加一 个构造函数但并没有意识到它会破坏你 mixin 到它里的类。

与子类化一样,这意味着需要考虑一个类是否允许用于 mixin。如果该类没有文档注释或明显的名称, 如 IterableMixin ,你应该假设你不能 mix 这个类。

对支持 mixin 的类在文档注明

在类的文档中要提到,这个类是否可以或必须用于 mixin 。如果你的类被设计只作为 mixin 使用, 那么考虑在类名以 Mixin 结尾。

构造函数

通过声明与类具有相同名称的函数以及附加可选的标识符来创建 Dart 构造函数。 后者附加标示符的 构造函数被称为命名构造函数

考虑 在类支持的情况下,指定构造函数为 const

如果一个类,它所有的字段都是 final ,并且构造函数出了初始化他们之外没有任 何其他操作,那么可以将其作为 const 构造函数。这样就能够允许用户在需要 常量的位置创建类的实例—一些大型的常量,switch case 语句,默认参数中, 以及其他的情况。

如果没有显示的指定为 const 构造函数,那么就无法实现上述目的。

但需要注意的是,构造函数被指定为 const ,那它就是公共 API 的一中承诺。 如果后面将构造函数更改为非 const ,那么在常量表达式中调用它的代码就会被破坏。 如果不想做出这样的承诺,那么就不要指定它为 const 构造函数。在实际运用中, const 构造函数对于简单的,不可变的数据记录类是非常有用的。

成员

成员属于对象,成员可以是方法或实例变量。

推荐 指定字段或顶级变量为 final

Linter rule: prefer_final_fields

状态不可变—随着时间推移状态不发生变化—有益于程序员推理。类和库中可变状态量越少,类和库 越容易维护。

当然,可变数据是非常有用的。但是,如果并不需要可变数据,应该尽可能默认指定字段和顶级变量为 final

对概念上是访问的属性使用 getter 方法。

判定一个成员应该是一个 getter 而不是一个方法是一件具有挑战性的事情。它虽然微妙,但对于好的 API 设计是非常重要的,也导致本规则会很长。其他的一些语言文化中回避了getter。他们只有在几乎 类似于字段访问的时候才会使用—它仅仅是根据对象的状态进行微小的计算。任何比这更复杂或 重量级的东西得到带有 () 的名字后面,给出一种”计算的操作在这!”信号。因为 . 后面只跟名称 意味着是”字段”。

Dart 与他们 同。在 Dart 中,所有点名称都可以是进行计算的成员调用。字段是特殊的— 字段的 getter 的实现是有语言提供的。换句话说,在 Dart 中,getter 不是”访问特别慢的字段”; 字段是”访问特别快的 getter “。

即便如此,选择 getter 而不是方法对于调用者来说是一个重要信号。信号大致的意思成员的操作 “类似于字段”。至少原则上可以这么认为,只要调用者清楚,这个操作可以使用字段来实现。这意味着:

  • 操作返回一个结果但不接受任何参数。

  • 调用者主要关系结果。 如果希望调用者关系操作产生结果的方式多于产生的结果,那么为操作 提供一个方法,使用描述工作的动词作为方法的名称。

    这并意味着操作必须特别快才能成为 getter 方法。IterableBase.length 复杂度是 O(n),是可以的。使用 getter 方法进行重要计算是没问题的。但是如果它做了大量的工作, 你可能需要通过一个描述其功能的动词的方法来引起使用者的注意。

    connection.nextIncomingMessage; // Does network I/O.
    expression.normalForm; // Could be exponential to calculate.
  • 操作不会产生使用者可见的副作用。 在程序中访问一个实际的字段不会改变对象或者其他的状态。 操作不会产生输出,写入文件等。同样 getter 方法也一样。

    注意关键字”使用者可见”。只要调用者不关心这些副作用。getter 方法可以修改隐藏状态或产生 带外副作用。 getter 方法可以惰性计算和存储他们的结果,写入缓存, log 等。这样是没有问题的。

    stdout.newline; // Produces output.
    list.clear; // Modifies object.
  • 操作是幂等的。 “幂等”是一个怪异的词,在这里可以理解为调用多次操作,除非在这些操作 调用之间某些状态被修改,否则每次操作都产生相同的结果。(很明显,如果在调用之间向列表添加 元素, list.length 会产生不同的结果。)

    这里”相同的结果”并不意味着 getter 方法必须一定要在每次调用成功后都返回相同的对象。如果 按这样的要求会迫使很过 getter 方法需要进行脆弱的缓存(brittle caching),这样就否定了 使用 getter 的全部意义。常见的非常好的示例是,每次调用一个 getter 方法返回一个新的 future 或 list。重点在于, future 完成后返回相同的值,list 包含了相同的元素。

    换句话说,调用者关系的是结果值应该相等。

    DateTime.now; // New result each time.
  • 结果对象不用公开所有原始对象的状态。 一个字段仅公开对象的一部分。如果操作返回的结果 公开了原始对象的整个状态,那么把该操作作为 to___()as___() 方法 可能会更好。

如果操作符合上述描述,那么它应该是一个 getter 方法。看似满足这一系列要求的成员并不多,但实际上 会超出你的想象。许多操作只是对某些状态进行一些计算,其中大多数能够,并且也应该作为 getter 方法。

rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;

对概念上是修改的属性使用 setter 方法。

Linter rule: use_setters_to_change_properties

判定一个成员应该是一个 setter 而不是一个方法与 getter 的判定一样。两者的操作都应该是 “类似于字段”的操作。

对于 setter 方法,”类似于字段”意味着:

  • 操作只有一个参数,不会返回结果。

  • 操作会更改对象中的某些状态。

  • 操作是幂等的。 使用相同的值调用相同的 setter 两次,就调用者而言,第二次不应该执 行任何操作。在内部,也许你会得到一些无效的缓存或者多次的日志记录。没关系,从调用者的角度 来看,第二次调用似乎没做任何事情。

rectangle.width = 3;
button.visible = false;

不要 在没有对应的 getter 的情况下定义 setter。

Linter rule: avoid_setters_without_getters

用户将 getter 和 setter 视为一个对象的可见属性。一个 “dropbox” 属性可以被写入但无法读 取,会令人感到困惑。并且也混淆了他们对属性如何工作的直观理解。 例如,没有 getter 的 setter 意味着你可以使用 = 来修改它,但却不能使用 +=

本规则意义并是说,你需要先添加一个 getter 才被允许添加 setter ,对象通常不应该暴露出 多余的状态。如果某个对象的某个状态可以修改但不能以相同的方式访问,请改用方法实现。

避免 从返回类型为 booldoubleintnum 的成员返回 null

Linter rule: avoid_returning_null

尽管在 Dart 中所有类型都可以为空,但用户几乎都不会考虑它们是 null 的情况。而小写命名是 源于 “Java primitive” 的提倡。

在 API 中有一个 “nullable primitive” 类型可能会偶尔被用到。例如,指出 map 中不存在的 key 值,但这样的应用并不多见。

如果确实有成员可能返回 null 的类型,请在文档中注明,以及在什么情况下回返回 null

避免 为了书写流畅,而从方法中返回 this

Linter rule: avoid_returning_this

方法级联是链接方法调用的更好的解决方式。

var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');
var buffer = StringBuffer()
    .write('one')
    .write('two')
    .write('three');

类型

程序中的类型用于约束流入代码各位置的的不同类型。类型会出现在两种位置:声明中的类型注解(type annotations)泛型调用(generic invocations)的类型参数。

当你想到静态类型时,通常会联想到类型注解。类型注解可以用于为变量,参数,字段,或者返回值 声明类型。在下面的示例中,boolString 是类型注解。他们位于代码静态声明结构的前面, 并且他们不会在运行时”执行”。

bool isEmpty(String parameter) {
  bool result = parameter.length == 0;
  return result;
}

泛型调用可以是一个字面量集合的定义,一个泛型类构造函数的调用,或者一个泛型方法的调用。在下面 的示例中,numint 都是泛型调用的类型参数。虽然它们是类型,但是它们也是第一类实体, 在运行时会被提升并传递给调用。

var lists = <num>[1, 2];
lists.addAll(List<num>.filled(3, 4));
lists.cast<int>();

这里再强调一下”泛型调用”,因为类型参数可以出现在类型注解中:

List<int> ints = [1, 2];

这里,int 是一个类型参数,但它出现在了类型注解中,而不是泛型调用。通常来说不需要担心这种情况, 但在几个地方,对于类型的运用是泛型调用而不是类型注解有不同的指导。

在大多数地方,Dart 允许省略类型注解并根据附近的上下文提供推断类型,或默认指定为 dynamic 类型。Dart 同时具有类型推断和 dynamic 类型的情况,导致对代码中 “untyped” 的含义产生一些 混淆。意思就是不类型就是动态类型吗?为避免这种混淆,应该避免说 “untyped” ,而是使用以下 术语:

  • 如果代码是类型注解,则在代码中显式写入类型。

  • 如果代码的类型是推断的,则不必写类型注解,Dart 会自己会找出它的类型。规则不考虑推断可能 会失败的情况,在一些地方,推理失败会产生一个静态错误。在其他情况下,Dart 使用 dynamic 作为备选类型。

  • 如果代码是动态类型,那么它的静态类型就是特殊的 dynamic 类型。代码可以明确地注解为 dynamic 类型,也可以由 Dart 进行推断。

换句话说,对于代码的类型是 dynamic 类型还是其他类型,在类型注解或类型推断中是正交的。

类型推断是一种强大的工具,可以免于编写和阅读那些明显或无趣的类型。在明显的情况下省略类型也会 引起读者注意那些被显式注解的重要类型,例如强制类型转换。

显示类型也是代码健壮和高可维护的关键。它们定义了API的静态形状。注明并约束流入代码各位置的值 的不同类型值。

这些准则促使我们在简洁性和明确性,灵活性和安全性之间找到了最佳平衡。在决定要编写类型钱前 您需要回答这两问题:

  • 我应该书写哪种类型那?因为我期望这些类型在代码中最好是被看到!
  • 我应该书写哪种类型那?因为推理无法为我提供这些类型!

这些规则可以帮助你回答第一个问题:

这些规则涵盖了第二个问题:

其余指南涵盖了和类型有关的其他具体问题。

推荐 为类型不明显的公共字段和公共顶级变量指定类型注解。

Linter rule: prefer_typing_uninitialized_variables

类型注解是关于如何使用库的重要文档。它们在程序的区域之间形成边界以隔离类型错误来源。思考下面代码:

install(id, destination) => ...

在这里,无法判断:这个 id 是什么,一个字符串?destination 又是什么,一个字符串还是一个 File 对象?方法是同步的还是异步的?下面的实例会清晰很多:

Future<bool> install(PackageId id, String destination) => ...

但在一些情况下,类型非常明显,根本没有指明类型的必要:

const screenWidth = 640; // Inferred as int.

这里的”明显”并没有精确的定义,下面这些可以作为很好的参考:

  • 字面量。
  • 构造函数调用。
  • 引用的其他类型明确的常量。
  • 数字和字符串的简单表达式。
  • 读者熟悉的工厂方法,如 int.parse()Future.wait() 等。

如有疑问,请添加类型注解。即使类型很明显,但可能任然希望明确的注解。如果推断类型依赖于其他库中的值 或声明,可能需要添加注解的声明。这样自己的API就不会因为其他库的修改而被悄无声息的改变了类型。

考虑 为类型不明显的私有字段和私有顶级变量指定类型注解。

Linter rule: prefer_typing_uninitialized_variables

为公共声明进行类型注解有助于使用代码的用户,为私有成员进行类型注解有助于代码的维护人员。 私有声明的范围较小,熟悉与它相关代码的人才需要知道它们的声明类型。在这里就更倾向于省略注解, 通过推理得到私有声明的类型。这也是为什么该规则相对于上一条更为柔和。

如果你认为初始化表达式—无论是什么表达式—足够清晰,那么可以省略它的注解。但是 如果你认为注解有助于使代码更清晰,那么你应该加上这个注解。

避免 为初始化的局部变量添加类型注解。

Linter rule: omit_local_variable_types

局部变量,特别是现代的函数往往很少,范围也很小。省略局部变量类型会将读者的注意力集中在变量的 名称及初始化值上。

List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  var desserts = <List<Ingredient>>[];
  for (var recipe in cookbook) {
    if (pantry.containsAll(recipe)) {
      desserts.add(recipe);
    }
  }

  return desserts;
}
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  List<List<Ingredient>> desserts = <List<Ingredient>>[];
  for (List<Ingredient> recipe in cookbook) {
    if (pantry.containsAll(recipe)) {
      desserts.add(recipe);
    }
  }

  return desserts;
}

如果局部变量没有初始值设定项,那么就无法判断它的类型了。这种情况下,最好是为变量加上类型注解。 否则,你的到的会是一个 dynamic 类型,并失去静态类型的好处。

List<AstNode> parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}

避免 在函数表达式上注解推断的参数类型。

匿名函数几乎都是作为一个回调参数类型立即传递给一个方法。(如果一个匿名函数没有立即使用,那么 有必要为它进行命名声明。)当在类型化上下文中创建函数表达式时,Dart 会尝试根据预期类型来推断 函数的参数类型。

例如,当为 Iterable.map() 传递一个函数表达式时,函数的参数类型会根据 map() 回调中所 期望的类型进行推断。

var names = people.map((person) => person.name);
var names = people.map((Person person) => person.name);

在极少数周围环境不明确的情况下,当无法为一个或多个函数提供参数类型时,需要为它们进行类型注解。

避免 在泛型调用中参数类型的冗余使用。

如果推断的类型结果与指定相同,那么参数指定就是多余的。如果泛型调用是初始化变量,或者是函数参数, 那么推断会自动为其填充类型:

Set<String> things = Set();
Set<String> things = Set<String>();

在这里,初始化过程中,构造函数参数的类型是通过变量的类型注解推断得到的。

在其他情况下,如果没有足够的信息来推断类型时,应该为参数添加类型注解:

var things = Set<String>();
var things = Set();

在这里,由于变量没有类型注解,因此没有足够的上下文来确定创建的 Set 是什么类型,因此应该显式 的提供参数类型。

在 Dart 推断类型错误的时候进行类型注解。

有时候,Dart 推断的并不是你期望的类型。例如,你可能希望初始化变量的类型是超类型(父类的类型), 以便后续可以为变量赋值一些同级别的其它变量:

num highScore(List<num> scores) {
  num highest = 0;
  for (var score in scores) {
    if (score > highest) highest = score;
  }
  return highest;
}
num highScore(List<num> scores) {
  var highest = 0;
  for (var score in scores) {
    if (score > highest) highest = score;
  }
  return highest;
}

在这里,如果 scores 中包含双精度数字,如 [1.2] ,那么 highest 的赋值会失败,因为 highest 的推断类型是 int ,而不是 num 。在这些情况下,就需要显式注解了。

推荐 使用 dynamic 注解替换推断失败的情况。

Dart 允许在许多地方省略类型注解,并尝试推断类型。在某些情况下,如果推断失败了,会默认指定为 dynamic 类型。如果 dynamic 类型与期望相同,那么从技术的角度来讲,这是获取类型最简洁 的方式。

但是,这种方式是最不清晰的。任何一个阅读代码的人,当看到一个类型确实的成员时,是没有办法 知道,编写的人是希望它是 dynamic 类型,还是期望它是其他的什么类型,或者阅读的人就简单的 认为是编写的人忘记了指定类型。

dynamic 是你期望的类型,就应该指明它,这样能让你的意图更清晰。

dynamic mergeJson(dynamic original, dynamic changes) => ...
mergeJson(original, changes) => ...

推荐 使 function 类型注解的特征更明显

成员类型注解标识符只有 Function ,注解标识符不包括任何返回值类型或参数类型,请参考 专门的 Function 类型说明。使用 Function 类型要稍微比使用 dynamic 更好些。 如果要使用 Function 来进行类型注解,注解类型应该包含函数的所有参数及返回值类型。

bool isValid(String value, bool Function(String) test) => ...
bool isValid(String value, Function test) => ...

此条规则有个例外,如果期望一个类型能够表示多种函数类型的集合。例如,我们希望接受的可能是一个参数 的函数,也可能是两个参数的函数。由于 Dart 没有集合类型,所以没有办法为类似成员精确的指定类型, 这个时候通常只能使用 dynamic。但这里使用 Function 要稍微比使用 dynamic 更有帮助些:

void handleError(void Function() operation, Function errorHandler) {
  try {
    operation();
  } catch (err, stack) {
    if (errorHandler is Function(Object)) {
      errorHandler(err);
    } else if (errorHandler is Function(Object, StackTrace)) {
      errorHandler(err, stack);
    } else {
      throw ArgumentError("errorHandler has wrong signature.");
    }
  }
}

不要 为 setter 方法指定返回类型。

Linter rule: avoid_return_types_on_setters

在 Dart 中,setter 永远返回 void 。为 setter 指定类型没有意义。

void set foo(Foo value) { ... }
set foo(Foo value) { ... }

不要 使用弃用的 typedef 语法。

Linter rule: prefer_generic_function_type_aliases

Dart 有两种为函数类型定义命名 typedef 注解语法。 原始语法如下:

typedef int Comparison<T>(T a, T b);

该语法有几个问题:

  • 无法为一个泛型函数类型指定名称。在上面的例子中,typedef 自己就是泛型。如果在代码中去 引用 Comparison 却不指定参数类型,那么你会隐式的得到一个 int Function(dynamic, dynamic) 类型的函数,而不是 int Function<T>(T, T) 。在实际应用中虽然不常用,但是在极少数 情况下是很重要的。

  • 参数中的单个标识符会被认为是参数名称,而不是参数类型。参考下面代码:

    typedef bool TestNumber(num);

    大多数用户希望这是一个接受 num 返回 bool 的函数类型。但它实际上是一个接受任何 对象(dynamic)返回 bool 的类型。 “num” 是参数名称( 它除了被用在 typedef 的 声明代码中,再也没有其他作用)。这个错误在 Dart 中存在了很长时间。

新语法如下所示:

typedef Comparison<T> = int Function(T, T);

如果想在方法中包含参数名称,可以这样做:

typedef Comparison<T> = int Function(T a, T b);

新语法可以表达旧语法所表达的任何内容,并且避免了单个标识符会被认为是参数类型的常见错误。同一个函数 类型语法(typedef 中 = 之后的部分)允许出现在任何类型注解可以能出现的地方。这样在程序的任何位置, 我们都可以以一致的方式来书写函数类型。

为了避免对已有代码产生破坏, typedef 的旧语法依旧支持。但已被弃用。

推荐 优先使用内联函数类型,而后是 typedef 。

Linter rule: avoid_private_typedef_functions

在 Dart 1中,如果要在字段,变量或泛型参数中使用函数类型,首选需要使用 typedef 定义这个类型。 Dart 2中任何使用类型注解的地方都可以使用函数类型声明语法:

class FilteredObservable {
  final bool Function(Event) _predicate;
  final List<void Function(Event)> _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 进行定义。但在大多数情况下,使用者 更希望知道函数使用时的真实类型,这样函数类型语法使它们清晰。

考虑 在参数上使用函数类型语法。

Linter rule: use_function_type_syntax_for_parameters

在定义参数为函数类型时, Dart 具有特殊的语法。与 C 类似,使用参数名称作为函数参数的函数名:

Iterable<T> where(bool predicate(T element)) => ...

在 Dart 2 添加函数类型语法之前,如果希望不通过 typedef 使用函数参数类型,上例是唯一的方法。 如今 Dart 已经可以为函数提供泛型注解,那么也可以将泛型注解用于函数类型参数中:

Iterable<T> where(bool Function(T) predicate) => ...

虽然新语法稍微冗长一点,但是你必须使用新语法才能与其他位置的类型注解的语法保持一致。

为类型是任何对象的参数使用 Object 注解,而不是 dynamic

某些操作适用于任何对象。例如,log() 方法可以接受任何对象,并调用对象上的 toString() 方法。 在 Dart 中两种类型可以表示所有类型:Objectdynamic 。但是,他们传达的意义并不相同。 和 Java 或 C# 类似,要表示成员类型为所有对象,使用 Object 进行注解。

使用 dynamic 释放出一种复杂的信号。它可能意味着成员的类型集合不足以使用 Dart 类型系统表达,或者 是变量来源于操作过程中,以及其他范围外的静态类型系统,或者是你明确的希望成员类型在 runtime 中动态 确定。

void log(Object object) {
  print(object.toString());
}

/// 返回一个表示 [arg] 参数的布尔值,[arg] 
/// 必须是字符串或布尔值。
bool convertToBool(dynamic arg) {
  if (arg is bool) return arg;
  if (arg is String) return arg == 'true';
  throw ArgumentError('Cannot convert $arg to a bool.');
}

使用 Future<void> 作为无法回值异步成员的返回类型。

对于不返回值得同步函数,要使用 void 作为返回类型。对于需要等待的,但无返回值的异步方法方法, 使用 Future<void> 作为返回值类型。

你可能会见到使用 FutureFuture<Null> 作为返回值类型,这是因为旧版本的 Dart 不允许 void 作为类型参数。既然现在允许了,那么就应该使用新的方式。使用新的方式能够更直接地匹配那些 已经指定了类型的同步函数,并在函数体中为调用者提供更好的错误检查。

对于一些异步函数,这些异步函数不会返回有用的值,而且不需要等待异步执行结束或不需要处理错误结果。 那么使用 void 作为这些异步函数的返回类型。

避免 使用 FutureOr<T> 作为返回类型。

如果一个方法接受了一个 FutureOr<int> 参数,那么参数接受的类型范围就会变大 。使用者 可以使用 int 或者 Future<int> 来调用这个方法,所以调用这个方法时就不用把 int 包装到一个 Future 中再传到方法中。而在方法中这个参数一定会进行被解包处理。

如果是返回一个 FutureOr<int> 类型的值,那么方法调用者在做任何有意义的操作之前,需要检查 返回值是一个 int 还是 Future<int> (或者调用者仅 await 得到一个值,却把它当做了 Future )。返回值使用 Future<int> ,类型就清晰了。一个函数要么一直异步,要么一直是同步, 这样才能够让调用者更容易理解,否则这个函数很难被正确的使用。

Future<int> triple(FutureOr<int> value) async => (await value) * 3;
FutureOr<int> triple(FutureOr<int> value) {
  if (value is int) return value * 3;
  return (value as Future<int>).then((v) => v * 3);
}

对这条规则更准确的描述是,*仅在逆变位置使用 FutureOr<T> *。参数是逆变(contravariant), 返回类型是协变(covariant)。在嵌套函数类型中,描述是相反的—如果一个参数自身就是函数参数类型,那么此时 回调函数的返回类型处于逆变位置,回调函数的参数是协变。这意味着回调中的函数类型可以返回 FutureOr<T>

Stream<S> asyncMap<T, S>(
    Iterable<T> iterable, FutureOr<S> Function(T) callback) async* {
  for (var element in iterable) {
    yield await callback(element);
  }
}

参数

在 Dart 中,可选参数可以是位置参数,也可以是命名参数,但不能两者都是。

避免 布尔类型的位置参数。

Linter rule: avoid_positional_boolean_parameters

与其他类型不同,布尔值通常以字面量方式使用。数字值的通常可以包含在命名的常量里,但对于布尔值通常 喜欢直接传 truefalse 。如果不清楚布尔值的含义,这样会造成调用者的代码不可读:

new Task(true);
new Task(false);
new ListBox(false, true, true);
new Button(false);

这里,应该考虑使用命名参数,命名构造函数或命名常量来阐明调用所执行的操作。

Task.oneShot();
Task.repeating();
ListBox(scroll: true, showScrollbars: true);
Button(ButtonState.enabled);

请注意,这并不适用于 setter ,因为 setter 的名称能够清楚的阐明值得含义:

listBox.canScroll = true;
button.isEnabled = false;

避免 在调用者需要省略前面参数的方法中,使用位置可选参数。

可选的位置参数应该具有逻辑性,前面参数应该比后面的参数使用更频繁。调用者不需要刻意的跳过或省略前面 的一个参数而为后面的参数赋值。如果需要省略前面参数,这种情况最好使用命名可选参数。

String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int end]);

DateTime(int year,
    [int month = 1,
    int day = 1,
    int hour = 0,
    int minute = 0,
    int second = 0,
    int millisecond = 0,
    int microsecond = 0]);

Duration(
    {int days = 0,
    int hours = 0,
    int minutes = 0,
    int seconds = 0,
    int milliseconds = 0,
    int microseconds = 0});

避免 强制参数去接受一个特定表示”空参数”的值。

如果调用者在逻辑上省略了参数,那么建议使用可选参数的方式让这些参数能够实际性的被省略,而不是 强制让调用者去为他们传入 null,或者空字符串,或者是一些其他特殊的值来表示该参数”不需要传值”。

省略参数更加简洁,也有助于防止在调用者偶然地将 null 作为实际值传递到方法中而引起 bug。

var rest = string.substring(start);
var rest = string.substring(start, null);

使用开始为闭区间,结束为开区间的半开半闭区间作为接受范围。

如果定义一个方法或函数来让调用者能够从某个整数索引序列中选择一系列元素或项,开始索引指向的元素 为选取的第一个元素,结束索引(可以为可选参数)指向元素的上一个元素为获取的最后一个元素。

这种方式与核心库一致。

[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'

在这里保持一致尤为重要,因为这些参数通常是未命名参数。如果你的 API 中第二个参数使用了长度值, 而不是结束索引,那么在调用端是无法区分两者之间的差异的。

相等

可能为类实现自定义相等的判定是比较棘手事情。用户对于对象的判等情况有着很深的直觉,同时像哈希表这样 的集合类型拥有一些细微的规则,包含在这些集合中的元素需要遵循这些规则。

对重写 == 操作符的类,重写 hashCode 方法。

Linter rule: hash_and_equals

默认的哈希实现为对象提供了一个身份哈希—如果两个对象是完全相同的,那么它们通常具有 相同的哈希值。同样,== 的默认行为是比较两个对象的身份哈希。

如果你重写 == ,就意味着你可能有不同的对象要让你的类认为是”相等的”。任何两个对象要相等就 必须必须具有相同的哈希值。 否则,这两个对象就无法被 map 和其他基于哈希的集合识别为等效对象。

== 操作符的相等遵守数学规则。

等价关系应该是:

  • 自反性: a == a 应该始终返回 true

  • 对称性: a == b 应该与 b == a 的返回值相同。

  • 传递性: If a == bb == c 都返回 true,那么 a == c 也应该返回 true

避免 为可变类自定义相等。

定义 == 时,必须要定义 hashCode 。两者都需要考虑对象的字段。如果这些字段发生了变化, 则意味着对象的哈希值可能会改变。

大多数基于哈希的集合是无法预料元素哈希值的改变—他们假设元素对象的哈希值是永远不变的, 如果元素哈希值发生了改变,可能会出现不可预测的结果。

不要 在自定义 == 操作符中检查 null

Linter rule: avoid_null_checks_in_equality_operators

Dart 指定此检查是自动完成的,只有当右侧不是 null 时才调用 == 方法。

class Person {
  final String name;
  // ···
  bool operator ==(other) => other is Person && name == other.name;

  int get hashCode => name.hashCode;
}
class Person {
  final String name;
  // ···
  bool operator ==(other) => other != null && ...
}