跳到主要内容
目录

Effective Dart:设计

目录 keyboard_arrow_down keyboard_arrow_up
more_horiz

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

命名

#

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

应该始终如一地使用术语

#

在您的代码中,对于同一事物使用相同的名称。如果您的 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 的其他部分。通过在这些基础上构建,您可以减少他们在变得高效之前必须获取的新知识量。

避免缩写

#

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

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

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

#

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

良好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.

考虑使代码读起来像一个句子

#

当对命名有疑问时,编写一些使用您的 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);

对于非布尔属性或变量,优先使用名词短语

#

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

良好dart
list.length
context.lineWidth
quest.rampagingSwampBeast
不良dart
list.deleteItems

对于布尔属性或变量,优先使用非祈使动词短语

#

布尔名称通常用作控制流中的条件,因此您需要一个在那里读起来不错的名称。比较

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

好的名称倾向于以几种动词之一开头

  • “是”的一种形式:isEnabledwasShownwillFire。到目前为止,这些是最常见的。

  • 助动词hasElementscanCloseshouldConsumemustSave

  • 主动动词:ignoresInputwroteFile。这些很少见,因为它们通常是模棱两可的。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);

对于布尔属性或变量,优先使用“肯定”名称

#

大多数布尔名称在概念上都有“肯定”和“否定”形式,其中前者感觉像是基本概念,而后者是其否定——“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());
}

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

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

对于主要目的是副作用的函数或方法,优先使用祈使动词短语

#

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

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

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

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

如果返回值是函数或方法的主要目的,则优先使用名词短语或非祈使动词短语

#

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

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

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

此指南刻意比之前的指南更柔和。有时,方法没有副作用,但仍然更易于使用动词短语(如 list.take()string.split())命名。

如果您想引起人们对函数或方法执行的工作的注意,请考虑使用祈使动词短语

#

当成员在没有任何副作用的情况下产生结果时,它通常应该是 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();

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

#

用户将在调用站点看到参数,因此在名称本身中也引用参数通常对可读性没有帮助。

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

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

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

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

#

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

  • 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 用于用作函数或类的方法的<强调>返回类型的类型。这不常见,但有时会出现在类型别名中,并且在实现访问者模式的类中出现

    良好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 = [];
}

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

#

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

优先使声明私有

#

库中的公共声明(无论是顶级的还是在类中的)都表示其他库可以并且应该访问该成员。这也是您的库方面的一个承诺,即支持它并在发生这种情况时正确运行。

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

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

#

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

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

当然,此指南并不意味着您<强调>应该将所有类放入一个巨大的单体库中,只是允许您将多个类放在一个库中。

类和混入

#

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

当一个简单的函数就可以完成任务时,避免定义单成员抽象类

#

Linter 规则:one_member_abstracts

与 Java 不同,Dart 具有一流的函数、闭包以及使用它们的简洁语法。如果您只需要类似回调的东西,只需使用函数即可。如果您正在定义一个类,并且它只有一个抽象成员,其名称没有意义(如 callinvoke),那么您很可能只需要一个函数。

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

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

#

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

避免扩展不打算被子类化的类

#

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

这两者都意味着类需要慎重考虑是否要允许子类化。这可以在文档注释中传达,或者通过为类提供一个明显的名称,如 IterableBase。如果类的作者没有这样做,最好假设您<强调>不应该扩展该类。否则,稍后对其进行的更改可能会破坏您的代码。

使用类修饰符来控制您的类是否可以被扩展

#

类修饰符(如 finalinterfacesealed)限制了类的扩展方式。例如,使用 final class A {}interface class B {} 来防止在当前库外部扩展。使用这些修饰符来传达您的意图,而不是依赖于文档。

避免实现不打算作为接口的类

#

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

但是,实现类的接口是对该类的非常紧密的耦合。这意味着几乎<强调>任何对其接口您正在实现的类的更改都将破坏您的实现。例如,向类添加新成员通常是安全的、非破坏性的更改。但是,如果您正在实现该类的接口,那么您的类现在会遇到静态错误,因为它缺少该新方法的实现。

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

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

使用类修饰符来控制您的类是否可以作为接口

#

在设计库时,使用类修饰符(如 finalbasesealed)来强制执行预期用法。例如,使用 final class C {}base class D{} 来防止在当前库外部实现。虽然理想情况下所有库都应使用这些修饰符来强制执行设计意图,但开发人员可能仍然会遇到未应用这些修饰符的情况。在这种情况下,请注意意外的实现问题。

相比于 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 语句、默认参数值等等内部。

如果您不显式地将其设为 const,他们就无法做到这一点。

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

成员

#

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

推荐:使字段和顶层变量为 final

#

Linter 规则:prefer_final_fields

不是可变的状态——不会随时间变化的状态——对于程序员来说更容易推理。 尽量减少可变状态的类和库往往更易于维护。 当然,拥有可变数据通常很有用。 但是,如果您不需要它,您的默认设置应该是尽可能使字段和顶层变量为 final

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

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

#

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

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

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

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

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

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

    不良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___() method。

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

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

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

#

Linter 规则:use_setters_to_change_properties

决定 setter 与 method 之间的选择类似于决定 getter 与 method 之间的选择。 在这两种情况下,操作都应该是“类似字段的”。

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

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

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

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

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

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

#

Linter 规则:avoid_setters_without_getters

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

本指南并意味着您应该添加 getter 只是为了允许您想要添加的 setter。 对象通常不应该暴露超出其需要的状态。 如果您有对象的某些状态可以修改但不能以相同的方式暴露,请使用 method 代替。

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

#

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

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

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

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

避免:没有初始化器的公共 late final 字段

#

与其他 final 字段不同,没有初始化器的 late final 字段确实定义了一个 setter。 如果该字段是公共的,则 setter 是公共的。 这很少是您想要的。 字段通常标记为 late,以便可以在实例生命周期的某个时刻在内部初始化它们,通常在构造函数主体内部。

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

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

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

#

当 API 返回容器类型时,它有两种方式来指示缺少数据:它可以返回一个空容器,也可以返回 null。 用户通常假设并偏好您使用空容器来指示“没有数据”。 这样,他们就拥有一个真实的对象,他们可以在其上调用 method,例如 isEmpty

为了指示您的 API 没有数据可提供,请优先返回空集合、可空类型的非空 future 或不发出任何值的 stream。

例外: 如果返回 null 意味着与产生空容器不同的东西,则使用可空类型可能是有意义的。

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

#

Linter 规则:avoid_returning_this

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

良好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;
}

泛型调用是集合字面量、对泛型类的构造函数的调用或对泛型 method 的调用。 在下一个示例中,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 是您想要的类型。

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

  • 优先注解顶层变量和字段,除非初始化器使类型显而易见。

对没有初始化器的变量进行类型注解

#

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

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

#

Linter 规则:type_annotate_public_apis

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

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

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

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

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

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

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

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

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

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

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

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

#

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

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

#

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

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

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

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

#

函数的参数列表确定了它与外部世界的边界。 注解参数类型使该边界明确定义。 请注意,即使默认参数值看起来像变量初始化器,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);
  }
}

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

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

#

Linter 规则:avoid_types_on_closure_parameters

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

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

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

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

#

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

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

#

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

注解变量也解决了本指南,因为现在类型参数推断出来的。

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

#

这与上一条规则相反。 如果调用的类型参数列表确实使用您想要的类型正确推断出来,则省略类型,让 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]);

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

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

#

编写类型注解或类型参数的目标是确定完整的类型。 但是,如果您编写泛型类型的名称但省略其类型参数,则您尚未完全指定类型。 在 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 类型注解。

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

在函数类型注解中优先使用签名

#

标识符 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.');
    }
  }
}

不要为 setter 指定返回类型

#

Linter 规则:avoid_return_types_on_setters

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

不良dart
void set foo(Foo value) {
   ...
}
良好dart
set foo(Foo value) {
   ...
}

不要使用旧式的 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 语法仍然受支持,以避免破坏现有代码,但它已被弃用。

优先使用内联函数类型而不是 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 可能仍然值得。 但在大多数情况下,用户希望在函数类型实际使用的地方看到它是什么,并且函数类型语法为他们提供了这种清晰度。

优先对参数使用函数类型语法

#

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() method 可以接受任何对象并对其调用 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 作为返回类型。 对于不产生值,但调用者可能需要等待的 method 的异步等效项是 Future<void>

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

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

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

#

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

如果您返回 FutureOr<int>,则用户需要检查是否返回 intFuture<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 中,可选参数可以是位置参数或命名参数,但不能两者兼有。

避免位置布尔参数

#

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,在 setter 中,名称清楚地表明了该值代表什么

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

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

#

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

良好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,
});

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

#

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

省略参数更简洁,并且有助于防止在用户认为他们正在提供真实值时意外传递哨兵值(如 null)的错误。

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

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

#

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

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

良好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

用户和使用 == 的代码期望所有这些定律都被遵守。如果你的类不能遵守这些规则,那么 == 就不是你尝试表达的操作的正确名称。

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

#

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