Effective Dart:设计
- 名称
- 请始终如一地使用术语
- 避免使用缩写
- 优先将最具描述性的名词放在最后
- 考虑使代码读起来像句子
- 对于非布尔属性或变量,优先使用名词短语
- 对于布尔属性或变量,优先使用非祈使动词短语
- 对于命名的布尔参数,考虑省略动词
- 对于布尔属性或变量,优先使用“肯定”名称
- 如果函数或方法的主要目的是副作用,则优先使用祈使动词短语
- 如果返回值是函数或方法的主要目的,则优先使用名词短语或非祈使动词短语
- 如果您想强调函数或方法执行的工作,则可以考虑使用祈使动词短语
- 避免以 get 开头命名方法
- 如果方法将对象的狀態复制到新对象,则优先命名为 to___()
- 如果方法返回由原始对象支持的不同表示形式,则优先命名为 as___()
- 避免在函数或方法的名称中描述参数
- 命名类型参数时,请遵循现有的助记符约定
- 库
- 类和混入
- 构造函数
- 成员
- 类型
- 请对没有初始化程序的变量进行类型注释
- 如果类型不明显,请对字段和顶层变量进行类型注释
- 不要冗余地对已初始化的局部变量进行类型注释
- 请在函数声明中注释返回值类型
- 请在函数声明中注释参数类型
- 不要在函数表达式中注释推断出的参数类型
- 不要对初始化形式进行类型注释
- 请在未推断出的泛型调用上编写类型参数
- 不要在已推断出的泛型调用上编写类型参数
- 避免编写不完整的泛型类型
- 请使用 dynamic 进行注释,而不是让推断失败
- 在函数类型注释中优先使用签名
- 不要为 setter 指定返回值类型
- 不要使用旧版类型定义语法
- 优先使用内联函数类型而不是类型定义
- 优先对参数使用函数类型语法
- 除非您想禁用静态检查,否则避免使用 dynamic
- 请使用 Future<void> 作为不生成值的异步成员的返回值类型
- 避免使用 FutureOr<T> 作为返回值类型
- 参数
- 相等性
以下是一些用于为库编写一致且易用的 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 的代码,并尝试将其读成句子。
// "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 并查看它在代码中“读取”的方式很有帮助,但您可能会做得太过火。添加文章和其他词性以迫使您的名称从字面上读起来像语法正确的句子并没有帮助。
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”:
isEnabled
、wasShown
、willFire
。这些是迄今为止最常见的。一个助动词:
hasElements
、canClose
、shouldConsume
、mustSave
。一个主动动词:
ignoresInput
、wroteFile
。这些很少见,因为它们通常模棱两可。loggedResult
是一个不好的名称,因为它可能表示“是否记录了结果”或“已记录的结果”。同样,closingConnection
可以是“连接是否正在关闭”或“正在关闭的连接”。当名称只能作为谓词读取时,允许使用主动动词。
将所有这些动词短语与方法名称区分开的是,它们不是祈使句。布尔名称永远不应该听起来像命令,告诉对象做某事,因为访问属性不会更改对象。(如果属性确实以有意义的方式修改了对象,则它应该是一个方法。)
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);
对于布尔属性或变量,优先使用“肯定”名称
#大多数布尔类型的名称在概念上都有“肯定”和“否定”形式,其中前者感觉像是基本概念,而后者是其否定——“打开”和“关闭”、“启用”和“禁用”等。通常,后者的名称字面上有一个前缀来否定前者:“可见”和“不可见”、“连接”和“断开连接”、“零”和“非零”。
在选择true
表示哪种情况(以及因此属性的命名依据)时,优先选择肯定的或更基本的情况。布尔类型的成员通常嵌套在逻辑表达式中,包括否定运算符。如果您的属性本身读起来像一个否定,那么读者在进行双重否定并理解代码含义时会更加困难。
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}
if (!socket.isDisconnected && !database.isEmpty) {
socket.write(database.read());
}
对于某些属性,没有明显的肯定形式。已刷新到磁盘上的文档是“已保存”还是“未更改”?未刷新过的文档是“未保存”还是“已更改”?在模棱两可的情况下,倾向于不太可能被用户否定的选择或名称较短的选择。
例外:对于某些属性,否定形式是用户绝大多数需要使用的。选择肯定情况将迫使他们在任何地方都使用!
来否定该属性。相反,对于该属性,最好使用否定情况。
如果函数或方法的主要目的是副作用,则优先使用祈使动词短语
#可调用成员可以将结果返回给调用方,并执行其他工作或副作用。在像 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
。例如,不要使用名为getBreakfastOrder()
的方法,而是定义一个名为breakfastOrder
的 getter。
即使该成员确实需要成为方法,因为它需要参数或不适合 getter,您也应该避免使用get
。就像前面的指南所述,可以:
简单地删除
get
并使用名词短语名称,例如breakfastOrder()
,如果调用方主要关心方法返回的值。使用动词短语名称,如果调用方关心正在执行的工作,但选择一个比
get
更准确地描述工作的动词,例如create
、download
、fetch
、calculate
、request
、aggregate
等。
如果方法将对象的内部状态复制到新对象中,则优先命名为to___()
#代码检查规则:use_to_and_as_if_applicable
转换方法是指返回一个新对象,该对象包含接收方几乎所有状态的副本,但通常以某种不同的形式或表示形式。核心库有一个约定,这些方法的名称以to
开头,后跟结果的类型。
如果您定义了一个转换方法,则遵循该约定很有帮助。
list.toSet();
stackTrace.toString();
dateTime.toLocal();
如果方法返回一个由原始对象支持的不同表示形式,则优先命名为as___()
#代码检查规则: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
表示集合中的元素类型良好dartclass IterableBase<E> {} class List<E> {} class HashSet<E> {} class RedBlackTree<E> {}
K
和V
表示关联集合中的键和值类型良好dartclass Map<K, V> {} class Multimap<K, V> {} class MapEntry<K, V> {}
R
表示用作函数或类的返回值的类型。这并不常见,但有时出现在类型定义中以及实现访问者模式的类中良好dartabstract class ExpressionVisitor<R> { R visitBinary(BinaryExpression node); R visitLiteral(LiteralExpression node); R visitUnary(UnaryExpression node); }
否则,对于具有单个类型参数并且周围类型使其含义显而易见的泛型,使用
T
、S
和U
。这里有多个字母,允许嵌套而不会隐藏周围的名称。例如良好dartclass 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 = [];
}
在实践中,现有的约定涵盖了大多数类型参数。
库
#前导下划线字符(_
)表示成员对其库是私有的。这不仅仅是约定,而是内置于语言本身。
优先使声明私有
#库中的公共声明(无论是顶级声明还是类中的声明)都表示其他库可以也应该访问该成员。这也是您库方面的一项承诺,即支持此操作并在发生时表现得体。
如果您不打算这样做,请添加一个小_
并感到高兴。狭窄的公共接口更易于您维护,也更易于用户学习。作为额外的好处,分析器会告诉您有关未使用的私有声明,以便您可以删除死代码。如果该成员是公共的,则它无法做到这一点,因为它不知道其视图之外的任何代码是否正在使用它。
考虑在同一个库中声明多个类
#一些语言(如 Java)将文件的组织与类的组织联系起来——每个文件只能定义一个顶级类。Dart 没有此限制。库是与类分开的独立实体。如果所有类在逻辑上都属于一起,则一个库包含多个类、顶级变量和函数是完全可以的。
将多个类放在一个库中可以启用一些有用的模式。由于 Dart 中的隐私在库级别而不是类级别起作用,因此这是一种定义“友元”类的方式,就像您在 C++ 中可能做的那样。在同一库中声明的每个类都可以访问彼此的私有成员,但该库外部的代码无法访问。
当然,此指南并不意味着您应该将所有类都放入一个巨大的单片库中,而只是表示您可以将多个类放在一个库中。
类和混入
#Dart 是一种“纯”面向对象的语言,因为所有对象都是类的实例。但是 Dart 并不要求所有代码都定义在类内部——您可以像在过程式或函数式语言中一样定义顶级变量、常量和函数。
如果简单的函数可以满足需求,则避免定义只有一个成员的抽象类
#代码检查规则:one_member_abstracts
与 Java 不同,Dart 具有头等函数、闭包以及使用它们的简洁语法。如果您只需要回调之类的东西,只需使用函数即可。如果您正在定义一个类,并且它只有一个抽象成员,并且名称毫无意义,例如call
或invoke
,那么很有可能您只需要一个函数。
typedef Predicate<E> = bool Function(E element);
abstract class Predicate<E> {
bool test(E element);
}
避免定义只包含静态成员的类
#代码检查规则:avoid_classes_with_only_static_members
在 Java 和 C# 中,每个定义都必须在类内部,因此常见的是看到仅作为存放静态成员的地方存在的“类”。其他类用作命名空间——一种为一堆成员提供共享前缀以将其相互关联或避免名称冲突的方法。
Dart 具有顶级函数、变量和常量,因此您不需要仅为了定义某些内容而使用类。如果您想要的是命名空间,则库更合适。库支持导入前缀和显示/隐藏组合器。这些是强大的工具,可以让您的代码使用者以最适合他们的方式处理名称冲突。
如果函数或变量在逻辑上不与类相关联,请将其放在顶级位置。如果您担心名称冲突,请为其提供更精确的名称或将其移动到可以使用前缀导入的单独库中。
DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
const _favoriteMammal = 'weasel';
class DateUtils {
static DateTime mostRecent(List<DateTime> dates) {
return dates.reduce((a, b) => a.isAfter(b) ? a : b);
}
}
class _Favorites {
static const mammal = 'weasel';
}
在惯用的 Dart 中,类定义对象类型。永远不会实例化的类型是代码异味。
但是,这不是硬性规定。例如,对于常量和类似枚举的类型,将它们分组到一个类中可能是很自然的。
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
避免扩展不打算被子类化的类
#如果构造函数从生成式构造函数更改为工厂构造函数,则调用该构造函数的任何子类构造函数都将中断。此外,如果类更改了它在this
上调用的方法,则可能会破坏覆盖这些方法并期望在某些点调用它们的子类。
这两者都意味着类需要谨慎考虑是否要允许子类化。这可以通过文档注释进行沟通,或者通过为类提供一个明显的名称(如IterableBase
)来进行沟通。如果类的作者没有这样做,最好假设您不应扩展该类。否则,以后对它的更改可能会破坏您的代码。
如果您的类支持扩展,请进行记录
#这是上述规则的推论。如果您想允许您的类的子类,请说明。在类名后添加Base
,或在类的文档注释中提及它。
避免实现不打算用作接口的类
#隐式接口是 Dart 中一个强大的工具,可以避免在类的契约可以从该契约实现的签名中轻松推断出来时重复该类的契约。
但是实现类的接口与该类耦合非常紧密。这意味着您正在实现的类的几乎*任何*更改都会破坏您的实现。例如,向类添加新成员通常是安全且不会破坏更改的。但是,如果您正在实现该类的接口,那么您的类现在会出现静态错误,因为它缺少该新方法的实现。
库维护人员需要能够改进现有类而不会破坏用户。如果您将每个类都视为公开了一个用户可以自由实现的接口,那么更改这些类就会变得非常困难。这种困难反过来意味着您依赖的库在增长和适应新需求方面速度较慢。
为了给您使用的类的作者提供更大的自由度,请避免实现隐式接口,除非这些类明确旨在被实现。否则,您可能会引入作者没有预期的耦合,并且他们可能会在未意识到的情况下破坏您的代码。
如果您的类支持用作接口,请进行记录
#如果您的类可以用作接口,请在类的文档注释中提及。
优先定义纯mixin
或纯class
而不是mixin class
#代码风格规则:prefer_mixin
Dart 之前(语言版本 2.12 到 2.19)允许任何满足某些限制条件的类(没有非默认构造函数、没有超类等)混合到其他类中。这令人困惑,因为类的作者可能没有打算将其混合进来。
Dart 3.0.0 现在要求任何打算混合到其他类中以及作为普通类处理的类型都必须使用mixin class
声明显式声明。
但是,需要同时作为 mixin 和类的类型应该很少见。mixin class
声明主要用于帮助将用作 mixin 的 3.0.0 之前的类迁移到更明确的声明。新代码应通过仅使用纯mixin
或纯class
声明来明确定义其声明的行为,并避免混合类带来的歧义。
阅读 将类迁移为 mixin 以获取有关mixin
和mixin class
声明的更多指导。
构造函数
#Dart 构造函数是通过声明一个与类同名的函数以及可选的附加标识符来创建的。后者称为*命名构造函数*。
考虑如果类支持,则将您的构造函数设为const
#如果您有一个类,其中所有字段都是final
,并且构造函数除了初始化它们之外什么都不做,则可以将该构造函数设为const
。这允许用户在需要常量的地方创建类的实例——在其他更大的常量、switch case、默认参数值等中。
如果您没有显式地将其设为const
,他们将无法做到这一点。
但是,请注意,const
构造函数是您公共 API 中的一个承诺。如果您稍后将构造函数更改为非const
,它将破坏在常量表达式中调用它的用户。如果您不想做出此承诺,请不要将其设为const
。在实践中,const
构造函数最适用于简单、不可变的值类型。
成员
#成员属于一个对象,可以是方法或实例变量。
优先将字段和顶层变量设为final
#代码风格规则:prefer_final_fields
对于程序员来说,推理*不可变*的状态(即状态不会随时间变化)更容易。尽量减少使用可变状态的类和库往往更容易维护。当然,拥有可变数据通常很有用。但是,如果您不需要它,那么在您可以的情况下,您的默认设置应该是将字段和顶层变量设为final
。
有时实例字段在初始化后不会更改,但直到实例构造完成后才能初始化。例如,它可能需要引用this
或实例上的其他一些字段。在这种情况下,请考虑将字段设为late final
。这样做时,您还可以在声明处初始化字段。
对于从概念上访问属性的操作,请使用 getter
#决定成员应该是 getter 还是方法是良好 API 设计中一个微妙但重要的部分,因此本指南非常长。其他一些语言的文化避开 getter。它们仅在操作几乎完全像字段时才使用它们——它对完全位于对象上的状态进行极少的计算。比这更复杂或更繁重的任何操作都会在名称后加上()
以表示“这里正在进行计算!”,因为.
后面的裸名表示“字段”。
Dart*不是*这样的。在 Dart 中,*所有*带点的名称都是可能进行计算的成员调用。字段是特殊的——它们是语言提供的实现的 getter。换句话说,在 Dart 中,getter 不是“特别慢的字段”;字段是“特别快的 getter”。
即使如此,选择 getter 而不是方法也会向调用方发送一个重要的信号。该信号大致是该操作“类似于字段”。至少在原则上,该操作*可以*使用字段实现,就调用方所知。这意味着
该操作不接受任何参数并返回结果。
**调用方主要关心结果。**如果您希望调用方更多地关注操作如何产生其结果而不是产生的结果,那么请为操作提供一个描述工作的动词名称并将其设为方法。
这*并不*意味着操作必须特别快才能成为 getter。
IterableBase.length
是O(n)
,这没问题。getter 可以进行大量计算。但是,如果它做了*令人惊讶*的工作量,您可能希望通过将其设为一个方法(其名称是描述其作用的动词)来提请他们注意这一点。不良dartconnection.nextIncomingMessage; // Does network I/O. expression.normalForm; // Could be exponential to calculate.
**该操作没有用户可见的副作用。**访问真实字段不会更改对象或程序中的任何其他状态。它不会产生输出、写入文件等。getter 也不应该做这些事情。
“用户可见”部分很重要。getter 可以修改隐藏状态或产生带外副作用。getter 可以延迟计算并存储其结果、写入缓存、记录内容等。只要调用方不*关心*副作用,它可能就很好。
不良dartstdout.newline; // Produces output. list.clear; // Modifies object.
**该操作是*幂等的*。**“幂等”是一个奇怪的词,在本上下文中,它基本上意味着多次调用该操作每次都会产生相同的结果,除非在这些调用之间显式修改了一些状态。(显然,如果您在调用之间向列表中添加元素,则
list.length
会产生不同的结果。)这里的“相同结果”并不意味着 getter 必须在连续调用时产生完全相同的对象。要求这样做会迫使许多 getter 具有脆弱的缓存,这会抵消使用 getter 的全部意义。getter 在每次调用时返回一个新的 future 或列表是很常见且完全正常的。重要的部分是 future 完成到相同的值,并且列表包含相同的元素。
换句话说,结果值应该在*调用方关心的方面*相同。
不良dartDateTime.now; // New result each time.
**结果对象不会公开原始对象的所有状态。**字段仅公开对象的一部分。如果您的操作返回一个结果,该结果公开了原始对象的所有状态,则将其设为
to___()
或as___()
方法可能更好。
如果以上所有内容都描述了您的操作,则它应该是一个 getter。似乎很少有成员能够通过这一考验,但令人惊讶的是,许多成员确实做到了。许多操作只是对某些状态进行一些计算,其中大部分可以并且应该是 getter。
rectangle.area;
collection.isEmpty;
button.canShow;
dataSet.minimumValue;
对于从概念上更改属性的操作,请使用 setter
#代码风格规则:use_setters_to_change_properties
在 setter 和方法之间进行选择类似于在 getter 和方法之间进行选择。在这两种情况下,操作都应该“类似于字段”。
对于 setter,“类似于字段”意味着
该操作接受单个参数并且不产生结果值。
该操作更改对象中的某些状态。
**该操作是幂等的。**使用相同的值两次调用相同的 setter 在调用方看来应该在第二次不做任何事情。在内部,您可能有一些缓存失效或日志记录正在进行。这没问题。但从调用方的角度来看,它看起来第二次调用什么也没做。
rectangle.width = 3;
button.visible = false;
不要在没有对应 getter 的情况下定义 setter
#代码风格规则:avoid_setters_without_getters
用户将 getter 和 setter 视为对象的可见属性。可以写入但不可见的“dropbox”属性令人困惑,并混淆了他们对属性工作原理的直觉。例如,没有 getter 的 setter 意味着您可以使用=
修改它,但不能使用+=
。
本指南*并不*意味着您应该添加一个 getter 只是为了允许您要添加的 setter。对象通常不应该公开超出其所需的状态。如果您对象的某个状态可以修改但不能以相同的方式公开,请改用方法。
避免使用运行时类型测试来伪造重载
#API 通常会支持对不同类型的参数执行类似的操作。为了强调这种相似性,一些语言支持*重载*,它允许您定义多个具有相同名称但参数列表不同的方法。在编译时,编译器会查看实际的参数类型以确定要调用哪个方法。
Dart 没有重载。您可以通过定义一个方法,然后在主体内部使用is
类型测试来查看参数的运行时类型并执行适当的行为来定义一个看起来像重载的 API。但是,以这种方式伪造重载会将*编译时*方法选择转变为在*运行时*发生的选项。
如果调用方通常知道他们拥有哪种类型以及他们想要哪种特定操作,最好定义具有不同名称的不同方法以允许调用方选择正确的操作。这提供了更好的静态类型检查和更快的性能,因为它避免了任何运行时类型测试。
但是,如果用户可能拥有未知类型的对象并且*希望*API 在内部使用is
来选择正确的操作,那么一个参数是所有支持类型超类型的方法可能是合理的。
避免没有初始化程序的公共late final
字段
#与其他final
字段不同,没有初始化程序的late final
字段*确实*定义了一个 setter。如果该字段是公共的,则 setter 也是公共的。这很少是您想要的。字段通常被标记为late
,以便它们可以在实例生命周期的某个时刻(通常在构造函数主体内部)在*内部*初始化。
除非您*确实*希望用户调用 setter,否则最好选择以下解决方案之一
- 不要使用
late
。 - 使用工厂构造函数计算
final
字段值。 - 使用
late
,但在其声明处初始化late
字段。 - 使用
late
,但将late
字段设为私有并为其定义一个公共 getter。
避免返回可空的 Future
、Stream
和集合类型
#当 API 返回容器类型时,它有两种方法来指示数据不存在:它可以返回一个空容器,或者它可以返回 null
。用户通常假设并更喜欢您使用空容器来指示“无数据”。这样,他们就拥有了一个真实的可以调用其方法(如 isEmpty
)的对象。
为了指示您的 API 没有数据提供,请优先返回空集合、可空类型的非空 Future 或不发出任何值的 Stream。
例外:如果返回 null
的含义与生成空容器不同,则使用可空类型可能是有意义的。
避免仅为了启用流畅接口而从方法中返回 this
#代码风格规则:avoid_returning_this
方法级联是用于链接方法调用的更好的解决方案。
var buffer = StringBuffer()
..write('one')
..write('two')
..write('three');
var buffer = StringBuffer()
.write('one')
.write('two')
.write('three');
类型
#当您在程序中写下类型时,您限制了流入代码不同部分的值的种类。类型可以出现在两种地方:声明上的类型注释和泛型调用的类型参数。
类型注释是您在想到“静态类型”时通常会想到的内容。您可以为变量、参数、字段或返回类型添加类型注释。在以下示例中,bool
和 String
是类型注释。它们挂在代码的静态声明结构上,并且在运行时不会“执行”。
bool isEmpty(String parameter) {
bool result = parameter.isEmpty;
return result;
}
泛型调用是集合字面量、对泛型类的构造函数的调用或对泛型方法的调用。在下一个示例中,num
和 int
是泛型调用上的类型参数。即使它们是类型,它们也是在运行时被具体化并传递给调用的第一类实体。
var lists = <num>[1, 2];
lists.addAll(List<num>.filled(3, 4));
lists.cast<int>();
我们在这里强调“泛型调用”部分,因为类型参数也可以出现在类型注释中
List<int> ints = [1, 2];
这里,int
是一个类型参数,但它出现在类型注释内,而不是泛型调用中。您通常不需要担心这种区别,但在几个地方,我们对类型在泛型调用中使用与在类型注释中使用时有不同的指导。
类型推断
#Dart 中的类型注释是可选的。如果您省略了一个,Dart 会尝试根据附近的上下文推断类型。有时它没有足够的信息来推断完整的类型。发生这种情况时,Dart 有时会报告错误,但通常会默默地用 dynamic
填充任何缺失的部分。隐式的 dynamic
会导致代码看起来像是推断的且安全的,但实际上完全禁用了类型检查。以下规则通过在推断失败时要求类型来避免这种情况。
Dart 同时拥有类型推断和 dynamic
类型的事实导致了一些关于说代码是“无类型的”的含义的混淆。这是否意味着代码是动态类型的,或者您没有编写类型?为了避免这种混淆,我们避免使用“无类型”一词,而是使用以下术语
如果代码是类型注释的,则该类型是在代码中显式编写的。
如果代码是推断的,则没有编写类型注释,并且 Dart 自己成功地推断出了类型。推断可能会失败,在这种情况下,指南不会认为它是推断的。
如果代码是动态的,则其静态类型是特殊的
dynamic
类型。代码可以显式注释为dynamic
,也可以推断出来。
换句话说,某些代码是注释的还是推断的与它是 dynamic
还是其他类型是正交的。
推断是一个强大的工具,可以省去您编写和阅读显而易见或不重要的类型的麻烦。它使读者将注意力集中在代码本身的行为上。显式类型也是健壮、可维护代码的关键部分。它们定义了 API 的静态形状,并创建边界来记录和强制执行允许哪些类型的值到达程序的不同部分。
当然,推断不是魔法。有时推断会成功并选择一个类型,但它不是您想要的类型。常见的情况是从变量的初始化程序推断出一个过于精确的类型,而您打算稍后将其他类型的赋值给该变量。在这些情况下,您必须显式编写类型。
此处的指南在我们发现的简洁性和控制性、灵活性和安全性之间取得了最佳平衡。有特定的指南涵盖所有各种情况,但粗略的总结是
即使
dynamic
是您想要的类型,也要在推断没有足够上下文时添加注释。除非需要,否则不要注释局部变量和泛型调用。
优先注释顶层变量和字段,除非初始化程序使类型变得显而易见。
请对没有初始化程序的变量进行类型注释
#代码风格规则:prefer_typing_uninitialized_variables
变量的类型(顶层、局部、静态字段或实例字段)通常可以从其初始化程序推断出来。但是,如果没有初始化程序,则推断会失败。
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;
}
如果类型不明显,请对字段和顶层变量进行类型注释
#代码风格规则: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 上的类型注释有助于您的代码的用户一样,私有成员上的类型也有助于维护人员。
不要冗余地对已初始化的局部变量进行类型注释
#代码风格规则: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;
}
有时推断的类型不是您希望变量具有的类型。例如,您可能打算稍后分配其他类型的值。在这种情况下,使用您想要的类型注释变量。
Widget build(BuildContext context) {
Widget result = Text('You won!');
if (applyPadding) {
result = Padding(padding: EdgeInsets.all(8.0), child: result);
}
return result;
}
请在函数声明中注释返回值类型
#与其他一些语言不同,Dart 通常不会从其主体推断函数声明的返回类型。这意味着您应该自己编写返回类型的类型注释。
String makeGreeting(String who) {
return 'Hello, $who!';
}
makeGreeting(String who) {
return 'Hello, $who!';
}
请注意,此指南仅适用于非局部函数声明:顶层、静态和实例方法以及 getter。局部函数和匿名函数表达式从其主体推断返回类型。实际上,匿名函数语法甚至不允许返回类型注释。
请在函数声明中注释参数类型
#函数的参数列表决定了它与外部世界的边界。注释参数类型使该边界得到明确定义。请注意,即使默认参数值看起来像变量初始化程序,Dart 也不会从其默认值推断可选参数的类型。
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);
}
}
例外:函数表达式和初始化形式参数具有不同的类型注释约定,如下一条指南中所述。
不要在函数表达式中注释推断出的参数类型
#代码风格规则:avoid_types_on_closure_parameters
匿名函数几乎总是立即传递给一个采用某种类型回调的方法。当在类型化上下文中创建函数表达式时,Dart 会尝试根据预期类型推断函数的参数类型。例如,当您将函数表达式传递给 Iterable.map()
时,您的函数的参数类型是根据 map()
预期的回调类型推断的
var names = people.map((person) => person.name);
var names = people.map((Person person) => person.name);
如果语言能够推断出您想要用于函数表达式中参数的类型,则不要注释。在极少数情况下,周围的上下文不够精确,无法为函数的一个或多个参数提供类型。在这些情况下,您可能需要添加注释。(如果该函数没有立即使用,通常最好将其命名为声明。)
不要对初始化形式进行类型注释
#代码风格规则:type_init_formals
如果构造函数参数正在使用 this.
初始化字段,或 super.
转发超级参数,则参数的类型被推断为与字段或超级构造函数参数的类型相同。
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});
}
请在未推断出的泛型调用上编写类型参数
#Dart 在推断泛型调用中的类型参数方面非常聪明。它查看表达式发生的位置的预期类型以及传递给调用的值的类型。但是,有时这些不足以完全确定类型参数。在这种情况下,请显式编写整个类型参数列表。
var playerScores = <String, int>{};
final events = StreamController<Event>();
var playerScores = {};
final events = StreamController();
有时调用会作为变量声明的初始化程序出现。如果变量不是局部的,则无需在调用本身写入类型参数列表,而可以在声明上添加类型注释
class Downloader {
final Completer<String> response = Completer();
}
class Downloader {
final response = Completer();
}
注释变量也解决了此指南,因为现在类型参数是推断的。
不要在已推断出的泛型调用上编写类型参数
#这是上一条规则的反面。如果调用的类型参数列表是使用您想要的类型正确推断的,则省略类型并让 Dart 为您完成工作。
class Downloader {
final Completer<String> response = Completer();
}
class Downloader {
final Completer<String> response = Completer<String>();
}
这里,字段上的类型注释提供了一个周围上下文来推断初始化程序中构造函数调用的类型参数。
var items = Future.value([1, 2, 3]);
var items = Future<List<int>>.value(<int>[1, 2, 3]);
这里,集合和实例的类型可以从其元素和参数自下而上推断出来。
避免编写不完整的泛型类型
#编写类型注释或类型参数的目标是确定完整的类型。但是,如果您编写了泛型类型的名称但省略了其类型参数,则您尚未完全指定类型。在 Java 中,这些称为“原始类型”。例如
List numbers = [1, 2, 3];
var completer = Completer<Map>();
这里,numbers
有一个类型注释,但注释没有为泛型 List
提供类型参数。同样,Completer
的 Map
类型参数也没有完全指定。在这样的情况下,Dart 不会尝试使用周围的上下文为您“填充”其余的类型。相反,它会默默地用 dynamic
(或类具有绑定时的绑定)填充任何缺失的类型参数。这很少是您想要的。
相反,如果您在类型注释中或在某个调用内的类型参数中编写泛型类型,请确保编写完整的类型
List<num> numbers = [1, 2, 3];
var completer = Completer<Map<String, int>>();
请使用 dynamic
添加注释,而不是让推断失败
#当推断无法填充类型时,通常默认为dynamic
。如果dynamic
正是您想要的类型,从技术上讲,这是获得它的最简洁方法。但是,它不是最清晰的方法。您的代码的普通读者看到注释缺失,无法知道您是否打算将其设为dynamic
,期望推断填充其他类型,还是仅仅忘记编写注释。
当dynamic
是您想要的类型时,请显式地写出来,以明确您的意图并突出显示此代码具有较低的静态安全性。
dynamic mergeJson(dynamic original, dynamic changes) => ...
mergeJson(original, changes) => ...
请注意,当 Dart *成功* 推断出dynamic
时,可以省略类型。
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
略有用处。如果您要添加注释,请首选包含函数参数和返回类型的完整函数类型。
bool isValid(String value, bool Function(String) test) => ...
bool isValid(String value, Function test) => ...
例外:有时,您需要一个表示多个不同函数类型联合的类型。例如,您可能接受一个带一个参数的函数或一个带两个参数的函数。由于我们没有联合类型,因此无法精确地对其进行类型化,并且通常需要使用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 指定返回值类型
#代码风格规则:avoid_return_types_on_setters
在 Dart 中,Setter 总是返回void
。编写这个词毫无意义。
void set foo(Foo value) { ... }
set foo(Foo value) { ... }
不要使用旧版类型定义语法
#代码风格规则: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)
。这在实践中并不常见,但在某些特殊情况下很重要。参数中的单个标识符被解释为参数的名称,而不是其类型。鉴于
不良darttypedef bool TestNumber(num);
大多数用户期望这是一个接受
num
并返回bool
的函数类型。它实际上是一个接受任何对象(dynamic
)并返回bool
的函数类型。参数的名称(除了在 typedef 中用于文档之外,没有其他用途)是“num”。这长期以来一直是 Dart 中错误的来源。
新的语法如下所示
typedef Comparison<T> = int Function(T, T);
如果要包含参数的名称,也可以这样做
typedef Comparison<T> = int Function(T a, T b);
新语法可以表达旧语法所能表达的一切,甚至更多,并且缺乏易出错的错误特性,其中单个标识符被视为参数的名称而不是其类型。typedef 中=
后的相同函数类型语法也允许出现在任何允许类型注释的地方,这为我们在程序中的任何位置编写函数类型提供了一种一致的方法。
旧的 typedef 语法仍然受支持,以避免破坏现有代码,但它已弃用。
优先使用内联函数类型而不是类型定义
#代码风格规则:avoid_private_typedef_functions
在 Dart 中,如果要将函数类型用于字段、变量或泛型类型参数,可以为函数类型定义一个 typedef。但是,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 仍然可能值得。但在大多数情况下,用户希望在使用函数类型的地方看到它的实际内容,而函数类型语法为他们提供了这种清晰度。
优先对参数使用函数类型语法
#代码风格规则:use_function_type_syntax_for_parameters
Dart 在定义类型为函数的参数时使用特殊语法。有点像在 C 中,您将参数的名称括在函数的返回类型和参数签名中
Iterable<T> where(bool predicate(T element)) => ...
在 Dart 添加函数类型语法之前,这是在不定义 typedef 的情况下为参数提供函数类型的唯一方法。现在 Dart 有了函数类型的通用表示法,您也可以将其用于函数类型的参数
Iterable<T> where(bool Function(T) predicate) => ...
新语法稍微冗长一些,但与必须使用新语法的其他位置一致。
除非您要禁用静态检查,否则请避免使用dynamic
#某些操作适用于任何可能的对象。例如,log()
方法可以接受任何对象并对其调用toString()
。Dart 中的两种类型允许所有值:Object?
和dynamic
。但是,它们传达了不同的含义。如果您只想声明允许所有对象,请使用Object?
。如果您想允许所有对象除了null
,则使用Object
。
类型dynamic
不仅接受所有对象,而且还允许所有操作。在编译时允许对类型为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
的现有 API 时,尤其是在泛型类型内部。例如,JSON 对象的类型为Map<String, dynamic>
,您的代码需要接受相同的类型。即使这样,在使用来自这些 API 之一的值时,通常最好在访问成员之前将其转换为更精确的类型。
对于不产生值的异步成员,请使用Future<void>
作为返回类型
#当您有一个不返回值的同步函数时,您使用void
作为返回类型。对于不产生值但调用者可能需要等待的异步方法,其异步等效项是Future<void>
。
您可能会看到使用Future
或Future<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>
。参数是逆变的,返回类型是协变的。在嵌套函数类型中,这会发生反转——如果您有一个参数其类型本身是一个函数,则回调的返回类型现在处于逆变位置,而回调的参数是协变的。这意味着回调的类型返回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 中,可选参数可以是位置参数或命名参数,但不能同时是两者。
避免使用位置布尔参数
#代码风格规则:avoid_positional_boolean_parameters
与其他类型不同,布尔值通常以字面形式使用。像数字这样的值通常包装在命名常量中,但我们通常直接传递true
和false
。如果不清楚布尔值代表什么,这可能会使调用站点难以阅读。
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,因为名称使值所代表的内容很清楚。
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
这样的哨兵值,而他们认为自己提供了真实值。
var rest = string.substring(start);
var rest = string.substring(start, null);
请使用包含起始值和排除结束值的参数来接受范围
#如果您正在定义一个方法或函数,让用户从某个整数索引序列中选择一系列元素或项目,请获取一个起始索引,它指的是第一个项目,以及一个(可能是可选的)结束索引,它比最后一个项目的索引大 1。
这与执行相同操作的核心库一致。
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
'abcd'.substring(1, 3) // 'bc'
在这里保持一致性尤其重要,因为这些参数通常是未命名的。如果您的 API 采用长度而不是端点,则在调用站点根本无法看到差异。
相等性
#为类实现自定义相等行为可能很棘手。用户对相等的工作原理有深刻的直觉,您的对象需要匹配这些直觉,并且像哈希表这样的集合类型有它们期望元素遵循的微妙约定。
如果您覆盖了==
,请覆盖hashCode
#代码风格规则:hash_and_equals
默认哈希码实现提供了一个标识哈希——两个对象通常只有在它们是完全相同的对象时才具有相同的哈希码。同样,==
的默认行为是标识。
如果你重写了==
,这意味着你的类可能拥有被视为“相等”的不同对象。**任何两个相等的对象必须具有相同的哈希码。** 否则,映射和其他基于哈希的集合将无法识别这两个对象是等价的。
请确保你的==
运算符遵循数学上的等式规则
#等价关系应该
**自反的**:
a == a
应该始终返回true
。**对称的**:
a == b
应该返回与b == a
相同的结果。**传递的**: 如果
a == b
和b == c
都返回true
,那么a == c
也应该返回true
。
使用==
的用户和代码都期望遵循所有这些规则。如果你的类无法遵守这些规则,那么==
就不是你试图表达的操作的正确名称。
避免为可变类定义自定义相等性
#代码风格检查规则:avoid_equals_and_hash_code_on_mutable_classes
当你定义==
时,你也必须定义hashCode
。这两者都应该考虑对象的字段。如果这些字段发生变化,则意味着对象的哈希码也可能发生变化。
大多数基于哈希的集合都不会预料到这一点——它们假设对象的哈希码将永远相同,如果事实并非如此,则可能会表现出不可预测的行为。
不要使==
的参数可为空
#代码风格检查规则:avoid_null_checks_in_equality_operators
语言规范规定null
仅等于自身,并且只有当右侧不是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;
}
除非另有说明,否则本网站上的文档反映的是 Dart 3.5.3。页面上次更新于 2024-11-06。 查看源代码 或 报告问题.