目录

“有效的 Dart:设计”

目录 keyboard_arrow_down keyboard_arrow_up
more_horiz

这里有一些编写一致且易用的库 API 的指南。

名称

#

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

始终如一地使用术语

#

在整个代码中,对同一事物使用相同的名称。如果您的 API 外部已经存在用户可能知道的先例,请遵循该先例。

gooddart
pageCount         // 一个字段。
updatePageCount() // 与 pageCount 一致。
toSomething()     // 与 Iterable 的 toList() 一致。
asSomething()     // 与 List 的 asMap() 一致。
Point             // 一个熟悉的概念。
baddart
renumberPages()      // 与 pageCount 混乱地不同。
convertToSomething() // 与 toX() 先例不一致。
wrappedAsSomething() // 与 asX() 先例不一致。
Cartesian            // 大多数用户不熟悉。

目标是利用用户已知的内容。这包括他们对问题领域本身的了解、核心库的约定以及您自己 API 的其他部分。通过在此基础上构建,您可以减少他们必须学习的新知识量,才能提高效率。

避免缩写

#

除非缩写比未缩写的术语更常见,否则不要缩写。如果确实缩写,请 正确地将其大写

gooddart
pageCount
buildRectangles
IOStream
HttpRequest
baddart
numPages    // “Num”是“number (of)”的缩写。
buildRects
InputOutputStream
HypertextTransferProtocolRequest

最具描述性的名词放在最后

#

最后一个词应该是对事物最具描述性的词。您可以用其他词(例如形容词)作为前缀来进一步描述事物。

gooddart
pageCount             // 一个计数(页数)。
ConversionSink        // 用于进行转换的接收器。
ChunkedConversionSink // 一个分块的 ConversionSink。
CssFontFaceRule       // CSS 中字体样式的规则。
baddart
numPages                  // 不是页面的集合。
CanvasRenderingContext2D  // 不是“二维的”。
RuleFontFaceCss           // 不是 CSS。

考虑使代码像句子一样可读

#

如果对命名有疑问,请编写一些使用您的 API 的代码,并尝试将其像句子一样阅读。

gooddart
// “如果 errors 为空…”
if (errors.isEmpty) ...

// “嘿,subscription,取消!”
subscription.cancel();

// “获取怪物,其中怪物有爪子。”
monsters.where((monster) => monster.hasClaws);
baddart
// 告诉 errors 清空自身,还是询问它是否为空?
if (errors.empty) ...

// 切换什么?切换到什么?
subscription.toggle();

// 过滤掉带有爪子的怪物,还是只包含那些怪物?
monsters.filter((monster) => monster.hasClaws);

尝试使用您的 API 并查看它在代码中的“阅读”方式很有帮助,但您可能会做得过火。添加冠词和其他词类以强制您的名称 逐字 像语法正确的句子一样阅读并没有帮助。

baddart
if (theCollectionOfErrors.isEmpty) ...

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

对于非布尔属性或变量,首选名词短语

#

读者的重点是属性 是什么 。如果用户更关心属性是如何确定的,那么它可能应该是一个带有动词短语名称的方法。

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

对于布尔属性或变量,首选非祈使动词短语

#

布尔名称通常用作控制流中的条件,因此您需要一个在其中读起来很好的名称。比较:

dart
if (window.closeable) ...  // 形容词。
if (window.canClose) ...   // 动词。

好的名称往往以几种动词开头:

  • “to be”的一种形式: isEnabledwasShownwillFire 。这些是迄今为止最常见的。

  • 一个 助动词hasElementscanCloseshouldConsumemustSave

  • 一个主动动词: ignoresInputwroteFile 。这些很少见,因为它们通常含糊不清。 loggedResult 是一个不好的名称,因为它可能意味着“是否记录了结果”或“已记录的结果”。同样, closingConnection 可能指的是“连接是否正在关闭”或“正在关闭的连接”。当名称 只能 被解读为谓词时,允许使用主动动词。

所有这些动词短语与方法名称的区别在于它们不是 祈使句 。布尔名称永远不应该听起来像是一个命令来告诉对象做某事,因为访问属性不会更改对象。(如果属性 确实 以有意义的方式修改了对象,则它应该是一个方法。)

gooddart
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
baddart
empty         // 形容词还是动词?
withElements  // 听起来好像它可能包含元素。
closeable     // 听起来像一个接口。
              // “canClose”作为句子读起来更好。
closingWindow // 返回布尔值还是窗口?
showPopup     // 听起来好像它显示了弹出窗口。

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

#

这完善了上一条规则。对于布尔类型的命名参数,在没有动词的情况下,名称通常同样清晰,并且在调用站点处代码的可读性更好。

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

首选布尔属性或变量的“肯定”名称

#

大多数布尔名称在概念上都有“肯定”和“否定”形式,前者感觉像是基本概念,而后者是它的否定——“打开”和“关闭”、“启用”和“禁用”等等。通常,后者的名称字面上都有否定前者的前缀:“可见”和“ 不可 见”、“已连接”和“ 已断开 连接”、“零”和“ 零”。

在选择 true 表示的两种情况中的哪一种——以及因此属性的命名方式——时,首选肯定的或更基本的一种。布尔成员通常嵌套在逻辑表达式中,包括否定运算符。如果您的属性本身读起来像一个否定,那么读者就很难在脑子里进行双重否定并理解代码的含义。

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

对于某些属性,没有明显的肯定形式。已刷新到磁盘的文档是“已保存”还是“ 未更改 ”? 刷新的文档是“ 保存”还是“已更改”?在模棱两可的情况下,倾向于不太可能被用户否定的选择或名称较短的选择。

例外: 对于某些属性,否定形式是用户最需要使用的。选择肯定的情况将迫使他们在任何地方都用 ! 来否定该属性。相反,对于该属性,最好使用否定形式。

如果函数或方法的主要目的是副作用,则首选祈使动词短语

#

可调用成员可以将结果返回给调用者,并执行其他工作或副作用。在像 Dart 这样的命令式语言中,成员通常主要用于它们的副作用:它们可能会更改对象的内部状态、产生一些输出或与外部世界进行通信。

这些类型的成员应该使用祈使动词短语命名,该短语阐明了成员执行的工作。

gooddart
list.add('element');
queue.removeFirst();
window.refresh();

这样,调用就读起来像是一个执行该工作的命令。

如果返回值是其主要目的,则首选名词短语或非祈使动词短语

#

其他可调用成员几乎没有副作用,但会将有用的结果返回给调用者。如果成员不需要参数来执行此操作,则它通常应该是 getter。但有时逻辑“属性”需要一些参数。例如, elementAt() 从集合中返回一部分数据,但它需要一个参数来知道要返回 哪一部分 数据。

这意味着成员在 语法上 是一个方法,但在 概念上 它是一个属性,应该使用描述成员返回 什么 的短语来命名。

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

本指南故意比上一条更柔和。有时,一个方法没有副作用,但仍然更容易用动词短语来命名,例如 list.take()string.split()

如果要提请注意其执行的工作,则考虑使用祈使动词短语

#

当成员在没有任何副作用的情况下产生结果时,它通常应该是 getter 或一个使用方法名词短语来描述其返回的结果的方法。但是,有时产生该结果所需的工作很重要。它可能容易出现运行时故障,或者使用重量级资源,例如网络或文件 I/O。在这种情况下,如果您希望调用者考虑成员正在执行的工作,请为成员提供一个动词短语名称来描述该工作。

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

但是,请注意,本指南比前两条更柔和。操作执行的工作通常是与调用者无关的实现细节,并且性能和健壮性边界会随着时间的推移而发生变化。大多数情况下,根据成员为调用者做了 什么 来命名您的成员,而不是它们 如何 做。

避免以 get 开头方法名

#

在大多数情况下,该方法应该是一个 getter,并且名称中已删除 get 。例如,不要使用方法名为 getBreakfastOrder() ,而是定义名为 breakfastOrder 的 getter。

即使成员确实需要是一个方法,因为它需要参数或者不适合作为 getter,您仍然应该避免使用 get 。正如之前的指南所述,或者:

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

  • 使用方法名词短语 ,如果调用者关心正在执行的工作,但选择一个比 get 更精确地描述工作的动词,例如 createdownloadfetchcalculaterequestaggregate 等。

如果方法将对象的状体复制到新对象,则首选命名方法为 to___()

#

Linter 规则:

转换 方法是一种返回新对象的方法,该新对象包含接收方几乎所有状态的副本,但通常以某种不同的形式或表示形式存在。核心库有一种约定,即这些方法的名称以 to 开头,后跟结果的类型。

如果您定义了转换方法,则遵循此约定很有帮助。

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

如果方法返回由原始对象支持的不同表示,则首选命名方法为 as___()

#

Linter 规则:

转换方法是“快照”。生成的 对象拥有原始对象状态的副本。还有一些类似转换的方法会返回 视图 ——它们提供一个新对象,但该对象会引用回原始对象。对原始对象的后续更改会反映在视图中。

您应该遵循的核心库约定是 as___()

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

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

#

用户将在调用站点看到参数,因此在名称本身中也引用它通常无助于提高可读性。

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

但是,提及参数可能有助于将其与采用不同类型的其他类似命名的 方法区分开来:

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

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

#

单个字母名称并非完全具有启发性,但几乎所有泛型类型都使用它们。幸运的是,它们大多以一致的、助记的方式使用它们。约定是:

  • 集合中的 元素 类型使用 E

    gooddart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • 关联集合中的 类型使用 KV

    gooddart
    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • 函数或类的返回类型使用 R 。这并不常见,但有时会出现在类型定义中,以及实现访问者模式的类中:

    gooddart
    abstract class ExpressionVisitor<R> {
      R visitBinary(BinaryExpression node);
      R visitLiteral(LiteralExpression node);
      R visitUnary(UnaryExpression node);
    }
  • 否则,对于具有单个类型参数且周围类型使其含义显而易见的泛型,请使用 TSU 。这里有多个字母,允许嵌套而不会隐藏周围的名称。例如:

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

    在这里,泛型方法 then<S>() 使用 S 来避免隐藏 Future<T> 上的 T

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

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

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

实际上,现有的约定涵盖了大多数类型参数。

#

前导下划线字符( _ )表示成员对其库是私有的。这不仅仅是约定,而是内置于语言本身的。

首选将声明设为私有

#

库中的公共声明(顶级声明或类中的声明)表示其他库可以并且应该访问该成员。这也是您的库方面的一项承诺,即支持这一点并在发生这种情况时表现得体。

如果您不打算这样做,请添加一个小 _ 并开心。狭窄的公共接口更易于维护,也更易于用户学习。作为一个不错的额外好处,分析器会告诉您未使用的私有声明,以便您可以删除死代码。如果成员是公开的,则无法做到这一点,因为它不知道其视图之外的任何代码是否正在使用它。

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

#

某些语言(例如 Java)将文件的组织与类的组织联系起来——每个文件只能定义一个顶级类。Dart 没有此限制。库是与类不同的独立实体。如果所有类在逻辑上都属于一起,则单个库完全可以包含多个类、顶级变量和函数。

将多个类放在一个库中可以启用一些有用的模式。由于 Dart 中的隐私是在库级别而不是类级别工作的,因此这是一种定义“友元”类的方式,就像您在 C++ 中可能做的那样。在同一库中声明的每个类都可以访问彼此的私有成员,但该库外部的代码则不能。

当然,本指南并不意味着您 应该 将所有类都放在一个巨大的单体库中,而只是意味着您可以将多个类放在一个库中。

类和混入

#

Dart 是一种“纯”面向对象的语言,因为所有对象都是类的实例。但是 Dart 不要求所有代码都必须在类内定义——您可以像在过程式或函数式语言中一样定义顶级变量、常量和函数。

如果简单函数可以实现,则避免定义单成员抽象类

#

Linter 规则:

与 Java 不同,Dart 具有头等函数、闭包和使用它们的简洁语法。如果您只需要回调之类的功能,只需使用函数即可。如果您正在定义一个类,并且它只有一个带有无意义名称(例如 callinvoke )的抽象成员,那么很有可能您只需要一个函数。

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

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

#

Linter 规则:

在 Java 和 C# 中,每个定义 必须 在类内,因此经常看到仅作为放置静态成员的地方存在的“类”。其他类用作命名空间——一种为一堆成员提供共享前缀以将它们相互关联或避免名称冲突的方法。

Dart 具有顶级函数、变量和常量,因此您不需要仅为了定义某些内容而使用类。如果您想要的是命名空间,则库更合适。库支持导入前缀和显示/隐藏组合器。这些是强大的工具,允许您的代码使用者以最适合 他们 的方式处理名称冲突。

如果函数或变量与类在逻辑上没有关联,请将其放在顶级位置。如果您担心名称冲突,请为其指定更精确的名称或将其移动到可以使用前缀导入的单独库中。

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

const _favoriteMammal = 'weasel';
baddart
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 中,类定义 对象类型 。永远不会实例化的类型是一种代码异味。

但是,这不是一个硬性规定。例如,对于常量和类似枚举的类型,将它们分组到一个类中可能是自然的。

gooddart
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 或纯 class ,而不是 mixin class

#

Linter 规则:

Dart 以前(语言版本 2.12

2.19) 允许满足某些限制(没有非默认构造函数、没有超类等)的任何类都可以混合到其他类中。这令人困惑,因为类的作者可能并不打算将其混合进去。

Dart 3.0.0 现在要求任何打算混合到其他类中的类型,以及被视为普通类的类型,都必须使用 mixin class 声明显式声明。

但是,需要同时作为混入和类的类型应该很少见。 mixin class 声明主要用于帮助将用作混入的 3.0.0 之前的类迁移到更明确的声明。新代码应该通过仅使用纯 mixin 或纯 class 声明来清晰地定义其声明的行为和意图,并避免混入类的歧义。

阅读 将类作为混入进行迁移 以获取有关 mixinmixin class 声明的更多指导。

构造函数

#

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

如果类支持,请考虑将构造函数设为 const

#

如果您有一个所有字段都是 final 的类,并且构造函数除了初始化它们之外什么也不做,则可以将该构造函数设为 const 。这允许用户在需要常量的地方创建类的实例——在其他更大的常量、switch 语句、默认参数值等内部。

如果您没有明确地将其设为 const ,则他们将无法做到这一点。

但是,请注意, const 构造函数是您公共 API 中的一项承诺。如果您稍后将构造函数更改为非 const ,则它将破坏在常量表达式中调用它的用户。如果您不想承诺这一点,请不要将其设为 const 。实际上, const 构造函数对于简单、不可变的值类型最有用。

成员

#

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

首选将字段和顶级变量设为 final

#

Linter 规则:

不是 可变 的——不会随时间变化——的状态更容易让程序员理解。尽量减少可变状态的类和库往往更容易维护。当然,拥有可变数据通常很有用。但是,如果您不需要它,则在您可以的情况下,默认情况下应将字段和顶级变量设为 final

有时,实例字段在初始化后不会更改,但在实例构造之后才能进行初始化。例如,它可能需要引用 this 或实例上的其他字段。在这种情况下,请考虑将字段设为 late final 。当您这样做时,您还可能能够 在其声明处初始化字段

对于概念上访问属性的操作,请使用 getter

#

决定成员应该是 getter 还是方法是一个微妙但重要的良好 API 设计部分,因此有这个非常长的指南。其他一些语言的文化避开 getter。它们只在操作几乎完全像字段时才使用它们——它对完全存在于对象上的状态进行极少量计算。比这更复杂或更繁重的事情会在名称后加上 () 来表示“这里正在进行计算!”,因为 . 后面的裸名称表示“字段”。

Dart 不是这样的。在 Dart 中, 所有 点状名称都是可能进行计算的成员调用。字段是特殊的——它们是其实现由语言提供的 getter。换句话说,在 Dart 中,getter 不是“特别慢的字段”;字段是“特别快的 getter”。

即便如此,选择 getter 而不是方法也会向调用者发送一个重要的信号。该信号大致是指操作是“类似字段的”。至少原则上,就调用者所知,操作 可以 使用字段来实现。这意味着:

  • 操作不接受任何参数并返回结果。

  • 调用者主要关心结果。 如果您希望调用者比生成的实际结果更关心操作如何产生结果,则为操作提供一个描述工作的动词名称并将其设为方法。

    并不 意味着操作必须特别快才能成为 getter。 IterableBase.lengthO(n) ,这没问题。getter 可以进行大量计算。但是,如果它做了 令人惊讶的 大量工作,您可能希望通过将其设为一个名称是动词的方法来提请他们的注意,该动词描述它所做的工作。

    baddart
    connection.nextIncomingMessage; // 执行网络 I/O。
    expression.normalForm; // 计算可能呈指数级增长。
  • 操作没有用户可见的副作用。 访问实际字段不会更改对象或程序中的任何其他状态。它不会产生输出、写入文件等。getter 也不能做这些事情。

    “用户可见”部分很重要。getter 可以修改隐藏状态或产生带外副作用。getter 可以延迟计算并存储其结果、写入缓存、记录内容等。只要调用者不 关心 副作用,它可能就没问题。

    baddart
    stdout.newline; // 产生输出。
    list.clear; // 修改对象。
  • **操作是 幂等的 。“幂等”是一个奇怪的词,在这个上下文中,基本上意味着多次调用操作每次都会产生相同的结果,除非在这些调用之间明确修改了某些状态。(显然,如果您在调用之间向列表中添加元素,则 list.length 会产生不同的结果。)

    这里的“相同结果”并不意味着 getter 必须在连续调用中字面地产生相同的对象。要求这样做将迫使许多 getter 具有脆弱的缓存,这会抵消使用 getter 的全部意义。每次调用它时,getter 返回新的 future 或列表是很常见且完全可以的。重要的是,future 完成为相同的值,并且列表包含相同的元素。

    换句话说,结果值应该在 调用者关心的方面 相同。

    baddart
    DateTime.now; // 每次结果都不同。
  • 结果对象不会公开原始对象的所有状态。 字段只公开对象的一部分。如果您的操作返回一个公开原始对象整个状态的结果,那么将其作为 to___()as___() 方法可能更好。

如果上述所有内容都描述了您的操作,则它应该是一个 getter。似乎很少有成员能够通过这一关,但令人惊讶的是,许多成员都能通过。许多操作只是对某些状态进行一些计算,并且其中大多数可以而且应该成为 getter。

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

对于概念上更改属性的操作,请使用 setter

#

Linter 规则:

在 setter 和方法之间做出决定类似于在 getter 和方法之间做出决定。在这两种情况下,操作都应该是“类似字段的”。

对于 setter,“类似字段的”意味着:

  • 操作接受一个参数并且不产生结果值。

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

  • 操作是幂等的。 从调用者的角度来看,用相同的值两次调用相同的 setter 应该在第二次什么也不做。在内部,您可能有一些缓存失效或日志记录正在进行。没关系。但从调用者的角度来看,第二次调用似乎什么也没做。

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

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

#

Linter 规则:

用户将 getter 和 setter 视为对象的可见属性。一个可以写入但不能看到的“dropbox”属性会令人困惑,并会混淆他们对属性如何工作的直觉。例如,没有 getter 的 setter 意味着您可以使用 = 来修改它,但不能使用 +=

本指南 并不 意味着您应该只为了添加要添加的 setter 而添加 getter。对象通常不应该公开比他们需要的更多的状态。如果您对象的某个状态可以修改但不能以相同的方式公开,请改用方法。

避免使用运行时类型测试来伪造重载

#

API 通常支持对不同类型的参数执行类似的操作。为了强调这种相似性,一些语言支持 重载 ,这允许您定义多个名称相同但参数列表不同的方法。在编译时,编译器会查看实际的参数类型以确定要调用哪个方法。

Dart 没有重载。您可以通过定义单个方法,然后在主体内部使用 is 类型测试来查看参数的运行时类型并执行适当的行为来定义看起来像重载的 API。但是,以这种方式伪造重载会将 编译时 方法选择转换为在 运行时 发生的选项。

如果调用者通常知道他们拥有哪种类型以及他们想要哪种特定操作,最好定义具有不同名称的不同方法,以允许调用者选择正确的操作。这提供了更好的静态类型检查和更快的性能,因为它避免了任何运行时类型测试。

但是,如果用户可能拥有未知类型的对象并且 希望 API 在内部使用 is 来选择正确的操作,那么参数是所有支持类型的超类型的一个方法可能是合理的。

避免没有初始化程序的公共 late final 字段

#

与其他 final 字段不同,没有初始化程序的 late final 字段 确实 定义了一个 setter。如果该字段是公共的,则 setter 是公共的。这很少是你想要的。字段通常标记为 late ,以便可以在实例生命周期的某个点(通常在构造函数体中) 在内部 进行初始化。

除非您 确实 希望用户调用 setter,否则最好选择以下解决方案之一:

  • 不要使用 late
  • 使用工厂构造函数来计算 final 字段值。
  • 使用 late ,但在其声明处初始化 late 字段。
  • 使用 late ,但使 late 字段为私有,并为其定义一个公共 getter。

避免返回可为空的 FutureStream 和集合类型

#

当 API 返回容器类型时,它有两种方法可以指示数据不存在:它可以返回空容器,也可以返回 null 。用户通常会假设并更喜欢您使用空容器来指示“无数据”。这样,他们就拥有一个可以对其调用像 isEmpty 这样的方法的真实对象。

为了指示您的 API 没有要提供的数据,请首选返回空集合、可为空类型的不可为空 future 或不发出任何值的流。 例外: 如果返回 null含义与 产生空容器不同,则使用可空类型可能是有意义的。

避免仅为了启用流畅接口而从方法返回 this

#

Linter 规则:

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

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

类型

#

当您在程序中写下类型时,您会限制流入代码不同部分的值的种类。类型可以出现在两种位置:声明上的 类型注解泛型调用的类型参数。

类型注解是您通常想到的“静态类型”。您可以为变量、参数、字段或返回类型添加类型注解。在以下示例中, boolString 是类型注解。它们挂在代码的静态声明结构上,不会在运行时“执行”。

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

泛型调用是集合字面量、对泛型类的构造函数的调用或对泛型方法的调用。在下一个示例中, numint 是泛型调用上的类型参数。即使它们是类型,它们也是在运行时被具体化并传递给调用的第一类实体。

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

我们在这里强调“泛型调用”部分,因为类型参数也可以出现在类型注解中:

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

这里, int 是一个类型参数,但它出现在类型注解中,而不是泛型调用中。您通常不必担心这种区别,但在某些情况下,对于在泛型调用中使用类型与在类型注解中使用类型,我们的指导有所不同。

类型推断

#

Dart 中类型注解是可选的。 如果您省略了一个,Dart 会尝试根据附近的上下文推断一个类型。有时它没有足够的信息来推断完整的类型。发生这种情况时,Dart 有时会报告错误,但通常会默默地用 dynamic 填充任何缺失的部分。隐式的 dynamic 会导致代码看起来是推断的并且安全的,但实际上完全禁用了类型检查。下面的规则通过在推断失败时要求类型来避免这种情况。

Dart 同时具有类型推断和 dynamic 类型,这导致人们对说代码是“无类型的”是什么意思感到困惑。这是否意味着代码是动态类型的,还是您没有 编写 类型?为了避免这种混淆,我们避免使用“无类型”的说法,而是使用以下术语:

  • 如果代码是 类型注解的 ,则该类型已在代码中显式编写。

  • 如果代码是 推断的 ,则没有编写类型注解,并且 Dart 自己成功地确定了类型。推断可能会失败,在这种情况下,指南不会认为这是推断的。

  • 如果代码是 动态的 ,则其静态类型是特殊的 dynamic 类型。代码可以显式注解为 dynamic ,也可以推断出来。

换句话说,某些代码是注解的还是推断的与它是否是 dynamic 或其他类型无关。

推断是一个强大的工具,可以帮助您避免编写和阅读明显或不重要的类型。它使读者的注意力集中在代码本身的行为上。显式类型也是健壮、可维护代码的关键部分。它们定义了 API 的静态形状,并创建边界以记录和强制执行允许哪些类型的值到达程序的不同部分。

当然,推断不是魔术。有时推断会成功并选择一个类型,但它不是您想要的类型。常见的情况是从变量的初始化程序推断出过于精确的类型,而您打算稍后将其他类型的赋值给该变量。在这些情况下,您必须显式编写类型。

这里的指南在我们发现的简洁性和控制性、灵活性和安全性之间取得了最佳平衡。有一些具体的指南涵盖了各种情况,但大致总结如下:

  • 即使 dynamic 是您想要的类型,如果推断没有足够的上下文,也要进行注解。

  • 除非需要,否则不要注解局部变量和泛型调用。

  • 除非初始化程序使类型显而易见,否则首选注解顶级变量和字段。

对于没有初始化程序的变量,请进行类型注解

#

Linter 规则:

变量(顶级变量、局部变量、静态字段或实例字段)的类型通常可以从其初始化程序推断出来。但是,如果没有初始化程序,则推断会失败。

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

如果类型不明确,请对字段和顶级变量进行类型注解

#

Linter 规则:

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

baddart
install(id, destination) => ...

这里, id 是什么还不清楚。字符串? destination 是什么?字符串还是 File 对象?此方法是同步的还是异步的?这更清晰:

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

但是,在某些情况下,该类型非常明显,以至于编写它是毫无意义的:

gooddart
const screenWidth = 640; // 推断为 int。

“明显”没有精确的定义,但这些都是很好的候选者:

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

如果您认为初始化程序表达式(无论是什么)都足够清晰,则可以省略注解。但是,如果您认为注解有助于使代码更清晰,则添加一个。

如有疑问,请添加类型注解。即使类型很明显,您可能仍然希望显式进行注解。如果推断的类型依赖于其他库的值或声明,您可能希望注解 的声明,以便对该其他库的更改不会在您没有意识到的情况下默默地更改您自己的 API 的类型。

此规则适用于公共和私有声明。正如 API 上的类型注解可以帮助您的代码的 用户 一样,私有成员上的类型可以帮助 维护人员

不要冗余地对已初始化的局部变量进行类型注解

#

Linter 规则:

局部变量,尤其是在函数倾向于很小的现代代码中,范围很小。省略类型可以使读者的注意力集中在变量的更重要的 名称 及其初始化值上。

gooddart
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;
}
baddart
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;
}

有时,推断的类型不是您希望变量具有的类型。例如,您可能打算稍后分配其他类型的变量。在这种情况下,请使用您想要的类型来注解变量。

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

对函数声明上的返回类型进行注解

#

与其他一些语言不同,Dart 通常不会从函数声明的主体推断函数的返回类型。这意味着您应该自己编写返回类型的类型注解。

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

请注意,此指南仅适用于 非局部 函数声明:顶级函数、静态函数和实例方法及 getter。局部函数和匿名函数表达式会从其主体推断返回类型。事实上,匿名函数语法甚至不允许返回类型注解。

对函数声明上的参数类型进行注解

#

函数的参数列表决定了它与外部世界的边界。注解参数类型使该边界定义明确。请注意,即使默认参数值看起来像变量初始化程序,Dart 也不会从其默认值推断可选参数的类型。

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

例外: 函数表达式和初始化形式参数具有不同的类型注解约定,如下两个指南所述。

不要对函数表达式上的推断参数类型进行注解

#

Linter 规则:

匿名函数几乎总是立即传递给采用某种类型回调的方法。 当在类型化上下文中创建函数表达式时, Dart 会尝试根据预期类型推断函数的参数类型。 例如,当您将函数表达式传递给 Iterable.map() 时,您的 函数的参数类型是根据 map() 预期回调的类型推断出来的:

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

如果语言能够推断出您想要用于函数表达式中参数的类型,则不要进行注解。在某些情况下,周围的

上下文不够精确,无法为函数的一个或多个参数提供类型。在这些情况下,您可能需要进行注解。(如果函数没有立即使用,通常最好 将其设为命名声明)。

不要对初始化形式参数进行类型注解

#

Linter 规则:

如果构造函数参数使用 this. 来初始化字段,或者使用 super. 来转发超参数,则参数的类型将被推断为分别与字段或超构造函数参数具有相同的类型。

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

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

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

对未推断的泛型调用编写类型参数

#

Dart 在推断泛型调用中的类型参数方面非常聪明。它会查看表达式出现的位置的预期类型以及传递给调用的值的类型。但是,有时这些信息不足以完全确定类型参数。在这种情况下,请显式编写整个类型参数列表。

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

有时,调用会作为变量声明的初始化程序出现。如果变量 不是 局部的,那么您可以不在调用本身编写类型参数列表,而是在声明上添加类型注解:

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

注解变量也可以解决此指南,因为现在 推断类型参数。

不要对已推断的泛型调用编写类型参数

#

这是上一条规则的反面。如果调用的类型参数列表 使用您想要的类型正确推断,则省略类型并让 Dart 为您完成工作。

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

在这里,字段上的类型注解提供了一个周围的上下文来推断初始化程序中构造函数调用的类型参数。

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

在这里,集合和实例的类型可以从其元素和参数自下而上地推断出来。

避免编写不完整的泛型类型

#

编写类型注解或类型参数的目标是确定完整的类型。但是,如果您编写泛型类型的名称但省略了其类型参数,则您尚未完全指定类型。在 Java 中,这些称为“原始类型”。例如:

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

在这里, numbers 有一个类型注解,但该注解没有为泛型 List 提供类型参数。同样, CompleterMap 类型参数也没有完全指定。在这种情况下,Dart 不会尝试使用周围的上下文为您“填写”类型的其余部分。相反,它会默默地用 dynamic (或类具有绑定时的绑定)填充任何缺失的类型参数。这很少是您想要的。

相反,如果您在类型注解中或在某些调用内的类型参数中编写泛型类型,请确保编写完整的类型:

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

使用 dynamic 进行注解,而不是让推断失败

#

当推断没有填充类型时,它通常默认为 dynamic 。如果 dynamic 是您想要的类型,这在技术上是获得它的最简洁方法。但是,这不是最 清晰 的方法。阅读您代码的普通读者看到注解缺失,无法知道您是否打算将其设为 dynamic ,期望推断填充其他类型,还是只是忘记了编写注解。

dynamic 是您想要的类型时,请显式编写它以使您的意图清晰,并突出显示此代码具有较低的静态安全性。

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

请注意,当 Dart 成功推断出 dynamic 时,省略类型是可以的。

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

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

在这里,Dart 为 json 推断 Map<String, dynamic> ,然后从中为 users 推断 dynamic 。可以将 users 保留为没有类型注解。这种区别有点微妙。允许推断将 dynamic 从其他地方的 dynamic 类型注解传播到您的代码中是可以的,但您不希望它在您的代码未指定类型注解的地方注入 dynamic 类型注解。

例外: 可以省略未使用的参数 (_) 上的类型注解。

首选函数类型注解中的签名

#

Function 标识符本身没有任何返回类型或参数签名,它指的是特殊的 Function 类型。此类型仅比使用 dynamic 略有用。如果您要进行注解,请首选包含函数的参数和返回类型的完整函数类型。

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

例外: 有时,您需要一个表示多个不同函数类型的联合的类型。例如,您可以接受一个接受一个参数的函数或一个接受两个参数的函数。由于我们没有联合类型,因此无法精确地对它进行类型化,您通常必须使用 dynamicFunction 至少比这更有用:

gooddart
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 规则:

在 Dart 中,setter 总是返回 void 。编写这个词是没有意义的。

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

不要使用旧的 typedef 语法

#

Linter 规则:

Dart 有两种表示法来为函数类型定义命名的 typedef。原始语法如下所示:

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

这种语法有几个问题:

  • 无法为 泛型 函数类型分配名称。在上面的示例中,typedef 本身是泛型的。如果您在代码中引用 Comparison ,而没有类型参数,则您隐式地获得了函数类型 int Function(dynamic, dynamic)而不是 int Function<T>(T, T) 。这在实践中并不经常出现,但在某些极端情况下很重要。

  • 参数中的单个标识符被解释为参数的 名称 ,而不是其 类型 。鉴于:

    baddart
    typedef bool TestNumber(num);

    大多数用户期望这是一个接受 num 并返回 bool 的函数类型。它实际上是一个接受 任何 对象 (dynamic) 并返回 bool 的函数类型。参数的 名称 (除了在 typedef 中用于文档之外,什么也不用于)是“num”。长期以来,这一直是 Dart 中错误的根源。

新的语法如下所示:

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

如果您想包含参数的名称,您也可以这样做:

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

新的语法可以表达旧语法可以表达的任何内容以及更多内容,并且缺少易于出错的错误特性,其中单个标识符被视为参数的名称而不是其类型。typedef 中 = 后面的相同函数类型语法也允许在允许类型注解的任何地方出现,这为我们在程序中的任何地方编写函数类型提供了一种一致的方法。

仍然支持旧的 typedef 语法以避免破坏现有代码,但它已弃用。

首选内联函数类型而不是 typedef

#

Linter 规则:

在 Dart 中,如果您想将函数类型用于字段、变量或泛型类型参数,您可以为函数类型定义 typedef。但是,Dart 支持可以在允许类型注解的任何地方使用的内联函数类型语法:

gooddart
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 规则:

在定义类型为函数的参数时,Dart 有一种特殊的语法。有点像在 C 中,您将参数的名称用函数的返回类型和参数签名括起来:

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

在 Dart 添加函数类型语法之前,这是在不定义 typedef 的情况下为参数提供函数类型的唯一方法。现在 Dart 具有函数类型的通用表示法,您也可以将其用于函数类型的参数:

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

新的语法稍微冗长一些,但与您必须使用新语法的其他位置一致。

除非您想禁用静态检查,否则避免使用 dynamic

#

某些操作可以使用任何可能的对象。例如, log() 方法可以接受任何对象并对其调用 toString() 。Dart 中的两种类型允许所有值: Object?dynamic 。但是,它们表达的意思不同。如果您只想声明允许所有对象,请使用 Object? 。如果您想允许所有对象(除了 null ),则使用 Object

dynamic 类型不仅接受所有对象,而且还允许所有 操作 。对 dynamic 类型值的任何成员访问在编译时都是允许的,但在运行时可能会失败并抛出异常。如果您想要这种冒险但灵活的动态分派,那么 dynamic 是正确的类型。

否则,请首选使用 Object?Object 。依靠 is 检查和类型提升来确保值的运行时类型支持您想要访问的成员,然后再访问它。

gooddart
/// 返回 [arg] 的布尔表示形式,它必须
/// 是字符串或布尔值。
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 的现有 API,尤其是在泛型类型内部。例如,JSON 对象的类型为 Map<String, dynamic> ,并且您的代码需要接受相同的类型。即便如此,在使用来自这些 API 之一的 value 时,在访问成员之前将其强制转换为更精确的类型通常是一个好主意。

Future<void> 用作不产生值的异步成员的返回类型

#

当您有一个不返回值的同步函数时,您可以使用 void 作为返回类型。对于不产生值但调用者可能需要等待的方法的异步等效项是 Future<void>

您可能会看到使用 FutureFuture<Null> 的代码,因为较旧版本的 Dart 不允许将 void 用作类型参数。现在它可以了,您应该使用它。这样做更直接地匹配您将如何键入类似的同步函数,并为您和函数主体的调用者提供更好的错误检查。

对于不返回值并且没有调用者需要等待异步工作或处理异步故障的异步函数,请使用 void 作为返回类型。

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

#

如果方法接受 FutureOr<int> ,则它 在它接受的内容方面很宽容 。用户可以使用 intFuture<int> 调用该方法,因此他们不需要将 int 包装在 Future 中,而您无论如何都会解包它。

如果您返回 FutureOr<int> ,则用户需要检查是否返回 intFuture<int> ,然后才能执行任何有用的操作。(或者他们只会 await 该值,有效地始终将其视为 Future 。)只需返回 Future<int> ,它更简洁。用户更容易理解函数是始终异步还是始终同步,但可以是任一者的函数很难正确使用。

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

此指南更精确的表述是仅在 逆变位置使用 FutureOr<T>。参数是逆变的,返回类型是协变的。在嵌套函数类型中,这会反转——如果您有一个参数其类型本身就是一个函数,那么回调的返回类型现在处于逆变位置,而回调的参数是协变的。这意味着回调的类型可以返回 FutureOr<T> 是可以的:

gooddart
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 规则:

与其他类型不同,布尔值通常以字面形式使用。数字之类的值通常包装在命名常量中,但我们通常直接传递 truefalse 。如果不清楚布尔值表示什么,这可能会使调用站点难以阅读:

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

相反,最好使用命名参数、命名构造函数或命名常量来阐明调用正在执行的操作。

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

请注意,这不适用于 setter,在 setter 中,名称可以清楚地表明该值表示什么:

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

如果用户可能想要省略较早的参数,则避免可选位置参数

#

可选位置参数应该具有逻辑上的进展,以便较早的参数比后面的参数更频繁地传递。用户几乎不需要显式传递“空洞”来省略较早的位置参数以传递后面的参数。最好为此使用命名参数。

gooddart
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 这样的哨兵值的情况,而用户认为他们正在提供真实值。

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

使用包含起始和排除结束参数来接受范围

#

如果您正在定义一个允许用户从某个整数索引序列中选择元素或项目的范围的方法或函数,请使用起始索引(指的是第一个项目)和(可能是可选的)结束索引(比最后一个项目的索引大 1)。

这与执行相同操作的核心库一致。

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

这里保持一致性尤其重要,因为这些参数通常是未命名的。如果您的 API 使用长度而不是端点,则在调用站点将完全看不到差异。

等式

#

为类实现自定义等式行为可能很棘手。用户对等式如何工作有深刻的直觉,您的对象需要匹配这些直觉,并且像哈希表这样的集合类型具有它们期望元素遵循的微妙约定。

如果您重写 == ,则重写 hashCode

#

Linter 规则:

默认哈希码实现提供 标识 哈希——两个对象通常只有在它们是完全相同的对象时才具有相同的哈希码。同样, == 的默认行为也是标识。

如果您正在重写 == ,这意味着您可能拥有被您的类认为是“相等”的不同对象。 任何两个相等的 objects 必须具有相同的哈希码。 否则,映射和其他基于哈希的集合将无法识别这两个对象是等效的。

使您的 == 运算符遵守等式的数学规则

#

等价关系应该是:

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

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

  • 传递性 :如果 a == bb == c 都返回 true ,则 a == c 也应该返回 true

使用 == 的用户和代码都期望遵循所有这些规则。如果您的类无法遵守这些规则,则 == 不是您尝试表达的操作的正确名称。

避免为可变类定义自定义等式

#

Linter 规则:

当您定义 == 时,您还必须定义 hashCode 。两者都应该考虑对象的字段。如果这些字段 发生更改 ,则意味着对象的哈希码可能会更改。

大多数基于哈希的集合并不会预料到这一点——它们假设对象的哈希码将永远相同,如果事实并非如此,则其行为可能会不可预测。

不要使 == 的参数可为空

#

Linter 规则:

语言规定 null 仅与其自身相等,并且只有在右侧不为 null 时才调用 == 方法。

gooddart
class Person {
  final String name;

  // ···

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

  // ···

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