目录

Effective Dart:设计

目录 keyboard_arrow_down keyboard_arrow_up
more_horiz

以下是一些关于为库编写一致且可用的 API 的指南。

命名

#

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

DO 始终如一地使用术语

#

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

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

目标是利用用户已经知道的内容。这包括他们对问题领域本身的了解、核心库的约定以及你自己的 API 的其他部分。通过在其基础上构建,你可以减少他们在能够高效工作之前必须获取的新知识量。

AVOID 缩写

#

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

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

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

#

最后一个词应该最能描述事物的本质。你可以在其前面加上其他词(例如形容词)来进一步描述事物。

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

CONSIDER 使代码读起来像一个句子

#

在命名方面有疑问时,请编写一些使用你的 API 的代码,并尝试像阅读句子一样阅读它。

dart
// "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);
dart
// 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 并查看它在代码中使用时如何“读取”是有帮助的,但你可能会走得太远。添加冠词和其他词性以强制你的名称字面上像语法正确的句子一样读取是没有帮助的。

dart
if (theCollectionOfErrors.isEmpty) ...

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

PREFER 对非布尔属性或变量使用名词短语

#

读者关注的重点在于属性什么。如果用户更关心属性是如何确定的,那么它应该是一个方法,并使用动词短语命名。

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

PREFER 对布尔属性或变量使用非祈使动词短语

#

布尔值名称通常在控制流中用作条件,因此你需要一个在此上下文中读起来流畅的名称。比较一下:

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

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

  • “to be” 的某种形式:isEnabled, wasShown, willFire。 这些是迄今为止最常见的。

  • 一个助动词hasElements, canClose, shouldConsume, mustSave

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

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

dart
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
dart
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.

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

#

这细化了之前的规则。对于作为布尔值的命名参数,通常省略动词名称同样清晰,并且在调用点代码的可读性更好。

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

PREFER 对布尔属性或变量使用“肯定”名称

#

大多数布尔值名称在概念上都有“肯定”和“否定”形式,前者感觉像是基本概念,后者是其否定——“open”和“closed”、“enabled”和“disabled”等。通常,后者的名称实际上带有否定前者的前缀:“visible”和“in-visible”、“connected”和“dis-connected”、“zero”和“non-zero”。

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

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

对于某些属性,没有明显的肯定形式。一个已刷新到磁盘的文档是“saved”还是“un-changed”?一个没有刷新的文档是“un-saved”还是“changed”?在有歧义的情况下,倾向于选择用户不太可能否定的或名称较短的那个。

例外: 对于某些属性,用户绝大多数情况下需要使用否定形式。选择肯定形式将迫使用户在所有地方都使用 ! 来否定该属性。相反,对于该属性,使用否定形式可能更好。

PREFER 对于主要目的是产生副作用的函数或方法使用祈使动词短语

#

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

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

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

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

PREFER 对于主要目的是返回值时,对函数或方法使用名词短语或非祈使动词短语

#

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

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

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

此准则特意比之前的准则要宽松。有时,一个方法没有副作用,但使用动词短语(如 list.take()string.split())命名仍然更简单。

CONSIDER 如果你想引起人们注意函数或方法所执行的工作,则使用祈使动词短语

#

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

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

但是请注意,此准则比前两个准则要宽松。操作执行的工作通常是一个与调用者无关的实现细节,并且性能和鲁棒性边界会随着时间而变化。大多数时候,根据它们为调用者做什么来命名成员,而不是它们如何做。

避免以 get 开头命名方法

#

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

即使该成员确实需要成为一个方法,因为它需要参数或以其他方式不适合 getter,你也应该避免使用 get。如前面的准则所述,要么

  • 简单地删除 get,如果调用者主要关心方法返回的值,则使用名词短语名称,如 breakfastOrder()

  • 如果调用者关心正在完成的工作,则使用动词短语名称,但选择一个比 get 更准确地描述工作的动词,例如 createdownloadfetchcalculaterequestaggregate 等。

如果方法将对象的状态复制到一个新对象,则优先将其命名为 to___()

#

Linter 规则:use_to_and_as_if_applicable

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

如果你定义了一个转换方法,那么遵循该约定会有所帮助。

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

如果方法返回由原始对象支持的不同表示形式,则优先将其命名为 as___()

#

Linter 规则:use_to_and_as_if_applicable

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

你要遵循的核心库约定是 as___()

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

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

#

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

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

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

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

DO 在命名类型参数时遵循现有的助记约定

#

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

  • E 表示集合中的元素类型

    dart
    class IterableBase<E> {}
    class List<E> {}
    class HashSet<E> {}
    class RedBlackTree<E> {}
  • KV 表示关联集合中的类型

    dart
    class Map<K, V> {}
    class Multimap<K, V> {}
    class MapEntry<K, V> {}
  • R 表示用作函数或类的方法的返回类型的类型。这不常见,但有时会出现在 typedef 中,也会出现在实现访问者模式的类中

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

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

    在此处,泛型方法 then<S>() 使用 S 以避免遮蔽 Future<T> 上的 T

如果以上任何一种情况都不适用,那么使用另一个单字母助记名称或描述性名称都可以

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

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

在实践中,现有约定涵盖了大多数类型参数。

#

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

PREFER 将声明设为私有

#

库中的公共声明(顶级声明或类中的声明)是一个信号,表明其他库可以并且应该访问该成员。这也是你的库承诺支持这一点并在发生这种情况时正常运行。

如果这不是你的意图,请添加小 _ 并享受它带来的好处。狭窄的公共接口更容易维护,也更容易让用户学习。作为额外的好处,分析器会告诉你未使用的私有声明,这样你就可以删除死代码。如果该成员是公共成员,它就无法做到这一点,因为它不知道其视图之外的任何代码是否正在使用它。

CONSIDER 在同一个库中声明多个类

#

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

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

当然,此准则并不意味着你应该将所有类放入一个巨大的单体库中,只是允许你将多个类放在一个库中。

类和混入

#

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

AVOID 当一个简单的函数就能满足需求时,定义一个单成员抽象类

#

Linter 规则:one_member_abstracts

与 Java 不同,Dart 具有头等函数、闭包和用于使用它们的漂亮轻量级语法。如果所需要的只是类似回调的功能,只需使用一个函数。如果你正在定义一个类,并且它只有一个抽象成员,并且该成员具有类似 callinvoke 这样的无意义的名称,那么你很可能只需要一个函数。

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

AVOID 定义一个仅包含静态成员的类

#

Linter 规则:avoid_classes_with_only_static_members

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

Dart 具有顶级函数、变量和常量,因此你不需要仅仅为了定义某些东西而使用类。如果你想要的是命名空间,那么库更合适。库支持导入前缀和 show/hide 组合符。这些强大的工具让你的代码的使用者能够以最适合他们的方式处理名称冲突。

如果函数或变量在逻辑上不与类相关联,则将其放在顶级。如果你担心名称冲突,请为其提供更精确的名称,或者将其移动到可以使用前缀导入的单独库中。

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

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

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

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

AVOID 扩展一个不打算被子类化的类

#

如果构造函数从生成式构造函数更改为工厂构造函数,则任何调用该构造函数的子类构造函数都将中断。此外,如果一个类更改了它在 this 上调用的自身方法,那么可能会破坏覆盖这些方法并期望在特定点调用它们的子类。

这两种情况都意味着类需要慎重考虑是否允许子类化。这可以在文档注释中说明,或者通过给类一个明显的名称,例如 IterableBase。如果类的作者没有这样做,最好假设你不应该扩展该类。否则,以后对其的更改可能会破坏你的代码。

DO 如果你的类支持被扩展,请加以说明

#

这是上述规则的必然结果。如果你想允许你的类的子类,请声明这一点。在类名后加上 Base 后缀,或者在类的文档注释中提及它。

AVOID 实现一个不打算作为接口的类

#

隐式接口是 Dart 中一个强大的工具,可以避免在可以从合约实现的签名中轻松推断出类合约时重复该合约。

但是,实现类的接口与该类紧密耦合。这意味着几乎任何对你正在实现的接口的类的更改都会破坏你的实现。例如,向类添加新成员通常是一个安全且非破坏性的更改。但是,如果你正在实现该类的接口,那么你的类现在会出现静态错误,因为它缺少新方法的实现。

库维护人员需要能够发展现有类而不破坏用户。如果你将每个类都视为它公开了一个用户可以自由实现的接口,那么更改这些类就会变得非常困难。这种困难反过来意味着你依赖的库增长和适应新需求的速度会变慢。

为了让您使用的类的作者有更多的回旋余地,请避免实现隐式接口,除非这些类明确旨在被实现。否则,你可能会引入作者不希望的耦合,他们可能会在没有意识到这一点的情况下破坏你的代码。

DO 如果你的类支持作为接口使用,请加以说明

#

如果你的类可以用作接口,请在类的文档注释中提及。

优先选择定义纯 mixin 或纯 class,而不是 mixin class

#

Linter 规则:prefer_mixin

Dart 之前(语言版本 2.122.19)允许将任何满足某些限制(没有非默认构造函数、没有超类等)的类混合到其他类中。这令人困惑,因为类的作者可能并没有打算将其混合使用。

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

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

请阅读 将类迁移为 mixin 以获得有关 mixinmixin class 声明的更多指导。

构造函数

#

Dart 构造函数是通过声明一个与类同名的函数以及可选的其他标识符来创建的。后者称为命名构造函数

如果类支持,请考虑使你的构造函数为 const

#

如果你的类中的所有字段都是 final,并且构造函数只进行初始化,则可以使该构造函数为 const。这允许用户在需要常量的地方创建你的类的实例——在其他更大的常量、switch case、默认参数值等内部。

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

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

成员

#

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

优先使字段和顶层变量为 final

#

Linter 规则:prefer_final_fields

不是可变的(不会随着时间而改变)的状态更容易让程序员理解。最大限度地减少他们使用的可变状态的类和库往往更容易维护。当然,拥有可变数据通常很有用。但是,如果不需要,你的默认值应该是在可以的情况下使字段和顶层变量为 final

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

DO 对概念上访问属性的操作使用 getter

#

决定何时将成员设为 getter 而不是方法是良好的 API 设计中一个微妙但重要的部分,因此有这个非常长的指南。其他一些语言的文化会避开 getter。它们仅在操作几乎与字段完全相同时才使用它们——它在完全位于对象上的状态上进行少量计算。任何比这更复杂或更繁重的东西都会在名称后加上 () 以表示“这里正在进行计算!”,因为 . 后的裸名称表示“字段”。

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

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

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

  • 调用者主要关心结果。如果你希望调用者更多地关注操作如何产生结果,而不是关注产生的结果,那么请给该操作一个描述工作的动词名称并将其设为方法。

    不是意味着该操作必须特别快才能成为 getter。IterableBase.lengthO(n),这是可以的。getter 进行大量计算是可以的。但是,如果它做了惊人地多的工作,你可能希望通过将其设为方法来引起他们的注意,该方法名称是一个描述它做什么的动词。

    dart
    connection.nextIncomingMessage; // Does network I/O.
    expression.normalForm; // Could be exponential to calculate.
  • 该操作没有用户可见的副作用。访问真实字段不会改变对象或程序中的任何其他状态。它不会产生输出、写入文件等。getter 也不应该做这些事情。

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

    dart
    stdout.newline; // Produces output.
    list.clear; // Modifies object.
  • 该操作是幂等的。“幂等”是一个奇怪的词,在这种情况下,基本上是指多次调用该操作每次都会产生相同的结果,除非在这些调用之间显式修改了某些状态。(显然,如果在调用之间向列表中添加元素,list.length 会产生不同的结果。)

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

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

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

如果以上所有描述都符合你的操作,则它应该是 getter。看起来很少有成员能通过这个考验,但令人惊讶的是,很多成员都能做到。许多操作只是对一些状态进行一些计算,其中大多数可以而且应该作为 getter。

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

DO 对概念上更改属性的操作使用 setter

#

Linter 规则:use_setters_to_change_properties

在 setter 和方法之间进行选择类似于在 getter 和方法之间进行选择。在这两种情况下,该操作都应该是“类似字段的”。

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

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

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

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

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

DON'T 定义一个没有相应 getter 的 setter

#

Linter 规则:avoid_setters_without_getters

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

本指南不是意味着你应该添加一个 getter 只是为了允许你想要添加的 setter。对象通常不应公开超出其需要的状态。如果你的对象的某个部分状态可以修改但不能以相同的方式公开,请改用方法。

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

#

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 或一个不发出任何值的 Stream。

例外:如果返回 null 的含义与产生空容器不同,那么使用可空类型可能是有意义的。

避免从方法返回 this 只是为了启用流畅的接口

#

Linter 规则:avoid_returning_this

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

dart
var buffer = StringBuffer()
  ..write('one')
  ..write('two')
  ..write('three');
dart
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

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

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

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

这里的指导方针在简洁性和控制、灵活性和安全性之间取得了我们发现的最佳平衡。有具体的指导方针来涵盖所有各种情况,但粗略的总结是

  • 当推断没有足够的上下文时,即使 dynamic 是你想要的类型,也要添加注解。

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

  • 除非初始化器使类型显而易见,否则最好注解顶级变量和字段。

DO 对没有初始值的变量进行类型注解

#

Linter 规则:prefer_typing_uninitialized_variables

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

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

DO 如果类型不明显,请对字段和顶层变量进行类型注解

#

Linter 规则:type_annotate_public_apis

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

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

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

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

但在某些情况下,类型是如此明显,以至于编写它毫无意义

dart
const screenWidth = 640; // Inferred as int.

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

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

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

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

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

DON'T 对已初始化的局部变量进行冗余的类型注解

#

Linter 规则:omit_local_variable_types

局部变量,尤其是在函数往往很小的现代代码中,作用域非常小。省略类型使读者的注意力集中在更重要的变量名称及其初始化值上。

dart
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;
}
dart
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;
}

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

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

DO 在函数声明上注解返回类型

#

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

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

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

DO 在函数声明上注解参数类型

#

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

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

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

DON'T 对函数表达式上推断的参数类型进行注解

#

Linter 规则:avoid_types_on_closure_parameters

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

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

如果语言能够为你想要的函数表达式中的参数推断类型,则不要添加注解。在极少数情况下,周围的上下文不够精确,无法为一个或多个函数的参数提供类型。在这些情况下,你可能需要添加注解。(如果该函数没有立即使用,通常最好将其设为命名声明。)

DON'T 对初始化形式参数进行类型注解

#

Linter 规则:type_init_formals

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

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

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

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

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

#

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

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

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

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

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

DON'T 对已推断的泛型调用写入类型参数

#

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

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

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

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

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

AVOID 写入不完整的泛型类型

#

编写类型注解或类型参数的目的是确定一个完整的类型。但是,如果你编写泛型类型的名称但省略其类型参数,则你没有完全指定类型。在 Java 中,这些被称为“原始类型”。例如

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

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

相反,如果你在类型注解中或在某个调用中作为类型参数编写泛型类型,请确保编写一个完整的类型。

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

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

#

当类型推断无法填充类型时,通常会默认为 dynamic。如果 dynamic 是你想要的类型,那么从技术上讲,这是获得它的最简洁的方法。然而,它不是最清晰的方式。你的代码的随意读者看到缺少注解时,无法知道你是否打算使用 dynamic,期望类型推断填充其他类型,还是只是忘记编写注解。

dynamic 是你想要的类型时,请显式地编写它,以明确你的意图并突出显示此代码的静态安全性较低。

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

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

dart
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 类型注解。

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

PREFER 函数类型注解中的签名

#

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

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

例外:有时,你想要一种表示多种不同函数类型联合的类型。例如,你可能接受一个带一个参数的函数或一个带两个参数的函数。由于我们没有联合类型,因此无法精确地键入该类型,你通常必须使用 dynamicFunction 至少比这更有帮助。

dart
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.');
    }
  }
}

DON'T 为 setter 指定返回类型

#

Linter 规则:avoid_return_types_on_setters

在 Dart 中,setter 始终返回 void。写这个词毫无意义。

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

DON'T 使用旧的 typedef 语法

#

Linter 规则:prefer_generic_function_type_aliases

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

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

该语法存在几个问题

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

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

    dart
    typedef bool TestNumber(num);

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

新语法如下所示

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

如果还要包含参数的名称,也可以这样做

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

新语法可以表达旧语法可以表达的任何内容以及更多内容,并且没有将单个标识符视为参数的名称而不是其类型的容易出错的错误特征。typedef 中 = 之后的相同函数类型语法也允许在类型注解可能出现的任何位置使用,为我们在程序中的任何位置编写函数类型提供了一种一致的方法。

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

PREFER 使用内联函数类型而不是 typedef

#

Linter 规则:avoid_private_typedef_functions

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

dart
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。但在大多数情况下,用户希望在函数类型实际使用的位置看到该函数类型是什么,而函数类型语法可以为他们提供这种清晰度。

PREFER 对参数使用函数类型语法

#

Linter 规则:use_function_type_syntax_for_parameters

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

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

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

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

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

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

#

某些操作可与任何可能的对象一起使用。例如,log() 方法可以接受任何对象并对其调用 toString()。Dart 中有两种类型允许所有值:Object?dynamic。但是,它们传达不同的含义。如果你只想声明你允许所有对象,请使用 Object?。如果你想允许除 null 之外的所有对象,请使用 Object

类型 dynamic 不仅接受所有对象,而且还允许所有操作。在编译时允许对 dynamic 类型的值进行任何成员访问,但可能会在运行时失败并抛出异常。如果你想要完全相同有风险但灵活的动态分派,则 dynamic 是要使用的正确类型。

否则,请首选使用 Object?Object。依赖 is 检查和类型提升来确保值的运行时类型在你访问成员之前支持你要访问的成员。

dart
/// 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 的现有 API 时,尤其是在泛型类型内部。例如,JSON 对象的类型为 Map<String, dynamic>,并且你的代码需要接受相同的类型。即便如此,在使用来自这些 API 之一的值时,在访问成员之前,通常最好将其强制转换为更精确的类型。

对于不产生值的异步成员,请使用 Future<void> 作为返回类型

#

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

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

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

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

#

如果方法接受 FutureOr<int>,则它在接受的内容方面是慷慨的。用户可以使用 intFuture<int> 调用该方法,因此他们无需将 int 包裹在你将要解包的 Future 中。

如果你返回 FutureOr<int>,则用户需要检查是返回 int 还是 Future<int>,然后才能执行任何有用的操作。(或者他们只会 await 该值,实际上始终将其视为 Future。)直接返回 Future<int>,这样更清晰。用户更容易理解一个函数始终是异步的还是始终是同步的,但可以同时是两种类型的函数很难正确使用。

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

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

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

参数

#

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

AVOID 位置布尔参数

#

Linter 规则:avoid_positional_boolean_parameters

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

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

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

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

请注意,这不适用于 setter,其中名称清楚地表明该值代表什么

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

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

#

可选的位置参数应具有逻辑上的递进关系,即较早的参数比后面的参数更常被传递。用户几乎不需要为了传递后面的位置参数而显式传递一个“空洞”来省略前面的位置参数。您最好为此使用命名参数。

dart
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});

AVOID 接受特殊“无参数”值的强制参数

#

如果用户在逻辑上省略一个参数,最好让他们通过使该参数成为可选参数来实际省略它,而不是强迫他们传递null、空字符串或其他表示“未传递”的特殊值。

省略参数更简洁,并有助于防止出现用户本以为提供了实际值,却意外传递了像 null 这样的哨兵值的错误。

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

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

#

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

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

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

在这里保持一致性尤其重要,因为这些参数通常是未命名的。如果您的 API 接受的是长度而不是结束点,那么在调用点根本看不出区别。

相等

#

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

如果重写了 ==,请务必重写 hashCode

#

Linter 规则:hash_and_equals

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

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

请务必使您的 == 运算符遵守数学上的相等性规则。

#

等价关系应该是:

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

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

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

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

AVOID 为可变类定义自定义相等

#

Linter 规则:avoid_equals_and_hash_code_on_mutable_classes

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

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

不要使 == 的参数可为空。

#

Linter 规则:avoid_null_checks_in_equality_operators

该语言指定 null 只等于它自己,并且只有当右侧不是 null 时,才会调用 == 方法。

dart
class Person {
  final String name;

  // ···

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

  // ···

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