目录

高效 Dart 语言指南:API 设计

目录 keyboard_arrow_down keyboard_arrow_up
more_horiz

下面给出的准则用于指导为库编写一致的、可用的 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中的一个强大工具,当一个类中可以很容易的推断出一些已经约定的有特征的实现时,隐式接口可以避免重复定义这个类的约定。

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

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

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

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

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

PREFER defining a pure mixin or pure class to a mixin class

对支持 mixin 的类在文档注明

Linter rule: prefer_mixin

Dart previously (language version 2.12 to 2.19) allowed any class that met certain restrictions (no non-default constructor, no superclass, etc.) to be mixed into other classes. This was confusing because the author of the class might not have intended it to be mixed in.

Dart 3.0.0 now requires that any type intended to be mixed into other classes, as well as treated as a normal class, must be explicitly declared as such with the mixin class declaration.

Types that need to be both a mixin and a class should be a rare case, however. The mixin class declaration is mostly meant to help migrate pre-3.0.0 classes being used as mixins to a more explicit declaration. New code should clearly define the behavior and intention of its declarations by using only pure mixin or pure class declarations, and avoid the ambiguity of mixin classes.

Read Migrating classes as mixins for more guidance on mixin and mixin class declarations.

构造函数

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

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

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

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

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

成员

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

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

Linter rule: prefer_final_fields

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

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

有时实例的某些字段在被初始化后不会再变化,但只能在实例被构造后才能被初始化。例如,某些字段可能需要引用 this。在这种情况下,请考虑将其声明为 late 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.
    
  • 注意关键字”使用者可见”。只要调用者不关心这些副作用。getter 方法可以修改隐藏状态或产生带外副作用。 getter 方法可以惰性计算和存储他们的结果,写入缓存, log 等。这样是没有问题的。

    这里”相同的结果”并不意味着 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 ,对象通常不应该暴露出多余的状态。如果某个对象的某个状态可以修改但不能以相同的方式访问,请改用方法实现。

AVOID using runtime type tests to fake overloading

It’s common for an API to support similar operations on different types of parameters. To emphasize the similarity, some languages support overloading, which lets you define multiple methods that have the same name but different parameter lists. At compile time, the compiler looks at the actual argument types to determine which method to call.

Dart doesn’t have overloading. You can define an API that looks like overloading by defining a single method and then using is type tests inside the body to look at the runtime types of the arguments and perform the appropriate behavior. However, faking overloading this way turns a compile time method selection into a choice that happens at runtime.

If callers usually know which type they have and which specific operation they want, it’s better to define separate methods with different names to let callers select the right operation. This gives better static type checking and faster performance since it avoids any runtime type tests.

However, if users might have an object of an unknown type and want the API to internally use is to pick the right operation, then a single method where the parameter is a supertype of all of the supported types might be reasonable.

AVOID public late final fields without initializers

Unlike other final fields, a late final field without an initializer does define a setter. If that field is public, then the setter is public. This is rarely what you want. Fields are usually marked late so that they can be initialized internally at some point in the instance’s lifetime, often inside the constructor body.

Unless you do want users to call the setter, it’s better to pick one of the following solutions:

  • Don’t use late.
  • Use a factory constructor to compute the final field values.
  • Use late, but initialize the late field at its declaration.
  • Use late, but make the late field private and define a public getter for it.

AVOID returning nullable Future, Stream, and collection types

When an API returns a container type, it has two ways to indicate the absence of data: It can return an empty container or it can return null. Users generally assume and prefer that you use an empty container to indicate “no data”. That way, they have a real object that they can call methods on like isEmpty.

To indicate that your API has no data to provide, prefer returning an empty collection, a non-nullable future of a nullable type, or a stream that doesn’t emit any values.

如果确实有成员可能返回 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.isEmpty;
  return result;
}

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

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

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

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

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

Type inference

Type annotations are optional in Dart. If you omit one, Dart tries to infer a type based on the nearby context. Sometimes it doesn’t have enough information to infer a complete type. When that happens, Dart sometimes reports an error, but usually silently fills in any missing parts with dynamic. The implicit dynamic leads to code that looks inferred and safe, but actually disables type checking completely. The rules below avoid that by requiring types when inference fails.

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

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

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

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

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

Inference is a powerful tool to spare you the effort of writing and reading types that are obvious or uninteresting. It keeps the reader’s attention focused on the behavior of the code itself. Explicit types are also a key part of robust, maintainable code. They define the static shape of an API and create boundaries to document and enforce what kinds of values are allowed to reach different parts of the program.

Of course, inference isn’t magic. Sometimes inference succeeds and selects a type, but it’s not the type you want. The common case is inferring an overly precise type from a variable’s initializer when you intend to assign values of other types to the variable later. In those cases, you have to write the type explicitly.

The guidelines here strike the best balance we’ve found between brevity and control, flexibility and safety. There are specific guidelines to cover all the various cases, but the rough summary is:

  • Do annotate when inference doesn’t have enough context, even when dynamic is the type you want.

  • Don’t annotate locals and generic invocations unless you need to.

  • Prefer annotating top-level variables and fields unless the initializer makes the type obvious.

DO type annotate variables without initializers

Linter rule: prefer_typing_uninitialized_variables

The type of a variable—top-level, local, static field, or instance field—can often be inferred from its initializer. However, if there is no initializer, inference fails.

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

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

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

Linter rule: type_annotate_public_apis

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

install(id, destination) => ...

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

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

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

const screenWidth = 640; // Inferred as int.

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

  • 字面量。

  • 构造函数调用。

  • 引用的其他类型明确的常量。

  • 数字和字符串的简单表达式。

  • 读者熟悉的工厂方法,如 int.parse()Future.wait() 等。

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

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

这条规则同时适用于公有和私有声明。就像 API 里的类型注释可以更好帮助代码的 用户,私有成员上的类型可以帮助 维护者

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

Linter rule: omit_local_variable_types

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

List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  var desserts = <List<Ingredient>>[];
  for (final 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 (final List<Ingredient> recipe in cookbook) {
    if (pantry.containsAll(recipe)) {
      desserts.add(recipe);
    }
  }

  return desserts;
}

Sometimes the inferred type is not the type you want the variable to have. For example, you may intend to assign values of other types later. In that case, annotate the variable with the type you want.

Widget build(BuildContext context) {
  Widget result = Text('You won!');
  if (applyPadding) {
    result = Padding(padding: EdgeInsets.all(8.0), child: result);
  }
  return result;
}

DO annotate return types on function declarations

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

String makeGreeting(String who) {
  return 'Hello, $who!';
}
makeGreeting(String who) {
  return 'Hello, $who!';
}

Note that this guideline only applies to named function declarations: top-level functions, methods, and local functions. Anonymous function expressions infer a return type from their body. In fact, the syntax doesn’t even allow a return type annotation.

DO annotate parameter types on function declarations

A function’s parameter list determines its boundary to the outside world. Annotating parameter types makes that boundary well defined. Note that even though default parameter values look like variable initializers, Dart doesn’t infer an optional parameter’s type from its default value.

void sayRepeatedly(String message, {int count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}
void sayRepeatedly(message, {count = 2}) {
  for (var i = 0; i < count; i++) {
    print(message);
  }
}

Exception: Function expressions and initializing formals have different type annotation conventions, as described in the next two guidelines.

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

Linter rule: avoid_types_on_closure_parameters

匿名函数几乎都是作为一个回调参数类型立即传递给一个方法。当在类型化上下文中创建函数表达式时,Dart 会尝试根据预期类型来推断函数的参数类型。

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

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

If the language is able to infer the type you want for a parameter in a function expression, then don’t annotate. In rare cases, the surrounding context isn’t precise enough to provide a type for one or more of the function’s parameters. In those cases, you may need to annotate. (If the function isn’t used immediately, it’s usually better to make it a named declaration.)

DON’T type annotate initializing formals

Linter rule: type_init_formals

If a constructor parameter is using this. to initialize a field, or super. to forward a super parameter, then the type of the parameter is inferred to have the same type as the field or super-constructor parameter respectively.

class Point {
  double x, y;
  Point(this.x, this.y);
}

class MyWidget extends StatelessWidget {
  MyWidget({super.key});
}
class Point {
  double x, y;
  Point(double this.x, double this.y);
}

class MyWidget extends StatelessWidget {
  MyWidget({Key? super.key});
}

DO write type arguments on generic invocations that aren’t inferred

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

var playerScores = <String, int>{};
final events = StreamController<Event>();
var playerScores = {};
final events = StreamController();

Sometimes the invocation occurs as the initializer to a variable declaration. If the variable is not local, then instead of writing the type argument list on the invocation itself, you may put a type annotation on the declaration:

class Downloader {
  final Completer<String> response = Completer();
}
class Downloader {
  final response = Completer();
}

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

DON’T write type arguments on generic invocations that are inferred

This is the converse of the previous rule. If an invocation’s type argument list is correctly inferred with the types you want, then omit the types and let Dart do the work for you.

class Downloader {
  final Completer<String> response = Completer();
}
class Downloader {
  final Completer<String> response = Completer<String>();
}

Here, the type annotation on the field provides a surrounding context to infer the type argument of constructor call in the initializer.

var items = Future.value([1, 2, 3]);
var items = Future<List<int>>.value(<int>[1, 2, 3]);

Here, the types of the collection and instance can be inferred bottom-up from their elements and arguments.

AVOID writing incomplete generic types

The goal of writing a type annotation or type argument is to pin down a complete type. However, if you write the name of a generic type but omit its type arguments, you haven’t fully specified the type. In Java, these are called “raw types”. For example:

List numbers = [1, 2, 3];
var completer = Completer<Map>();

Here, numbers has a type annotation, but the annotation doesn’t provide a type argument to the generic List. Likewise, the Map type argument to Completer isn’t fully specified. In cases like this, Dart will not try to “fill in” the rest of the type for you using the surrounding context. Instead, it silently fills in any missing type arguments with dynamic (or the bound if the class has one). That’s rarely what you want.

Instead, if you’re writing a generic type either in a type annotation or as a type argument inside some invocation, make sure to write a complete type:

List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();

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

When inference doesn’t fill in a type, it usually defaults to dynamic. If dynamic is the type you want, this is technically the most terse way to get it. However, it’s not the most clear way. A casual reader of your code who sees that an annotation is missing has no way of knowing if you intended it to be dynamic, expected inference to fill in some other type, or simply forgot to write the annotation.

When dynamic is the type you want, write that explicitly to make your intent clear and highlight that this code has less static safety.

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

Note that it’s OK to omit the type when Dart successfully infers dynamic.

Map<String, dynamic> readJson() => ...

void printUsers() {
  var json = readJson();
  var users = json['users'];
  print(users);
}

Here, Dart infers Map<String, dynamic> for json and then from that infers dynamic for users. It’s fine to leave users without a type annotation. The distinction is a little subtle. It’s OK to allow inference to propagate dynamic through your code from a dynamic type annotation somewhere else, but you don’t want it to inject a dynamic type annotation in a place where your code did not specify one.

Exception: Type annotations on unused parameters (_) can be omitted.

推荐 使 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 (final 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) => ...

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

避免 使用 dynamic 除非你希望禁用静态检查

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

dynamic 这个类型不仅接受所有对象,也允许所有 operations。在编译时任何成员对 dynamic 类型值访问是允许的,但在运行时可能会引发异常。如果你可以承担风险来达到灵活性,dynamic 类型是你不错的选择。

除此之外,我们建议你使用 Object? 或者 Object,并使用 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.');
}

这个规则的主要例外是,与已经使用 dynamic 的类型,特别是通用类进行操作的时候。比如,JSON 对象有 Map<String, dynamic> 类型,而且代码需要接受相同的类型。即便如此,在调用和使用这些 API 的时候,将类型转换成一个更精确的类型之后再去调用成员会更好。

使用 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.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 (final 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

Users and code that uses == expect all of these laws to be followed. If your class can’t obey these rules, then == isn’t the right name for the operation you’re trying to express.

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

Linter rule: avoid_equals_and_hash_code_on_mutable_classes

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

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

不要 使用 == 操作符与可空值比较。

Linter rule: avoid_null_checks_in_equality_operators

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

class Person {
  final String name;
  // ···

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

  bool operator ==(Object? other) =>
      other != null && other is Person && name == other.name;
}