Style
UpperCamelCase:每个单词的首字母大写,包括第一个字母。
lowerCamelCase:每个单词的首字母大写,第一个字母总是小写,即使首字母缩写也是如此。
lowercase_with_underscores:仅使用小写字母(即使是首字母缩写词),并使用_分隔单词。
标识符
UpperCamelCase:
Classes, enum types, typedefs, type parameters(泛型中的类型) ,extensions methods
lowercase_with_underscores:
libraries, packages, directories, source files,import prefixes(import .. as 导入前缀)
lowerCamelCase:
Class members, top-level definitions, variables, parameters, named parameters,constant names
缩写词:
只有两个字母的缩写词作整个标识符,首字母大写 Id
只有两个字母的缩写词作为标识符一部分,字母大写 IOStream DBIOPort
多个字母的缩写词,首字母大写 HttpRequest
下划线:
前导下划线和私有相关,尽量不使用,例外:未使用的参数可以命名为_,__,___等。这种情况发生在回调中,在这些回调中您可以向其传递一个值,但不需要使用它。 给它一个仅包含下划线的名称是一种惯用的方式,用于指示未使用该值。
不要使用前缀小写字母,例如 kDefaultTimeout
排序
//首先是Dart核心库
import 'dart:async';
import 'dart:html';
//然后是其他包
import 'package:bar/bar.dart';
import 'package:foo/foo.dart';
//相对导入
import 'util.dart';
//其他相对文件
import 'src/error.dart';
import 'src/foo_bar.dart';
//导出
export 'src/error.dart';
各个部分使用空行分割,各个部分之中按照字母排序
一行不要超过80个字符
dartfmt代码格式化
例外:当URI或文件路径出现在注释或字符串中(通常在导入或导出中)时,即使它导致行超过80个字符,也可能保持完整。 这样可以更轻松地在源文件中搜索路径。
例外:多行字符串可以包含多于80个字符的行,因为换行符在字符串内很重要,将行拆分为较短的行可以更改程序。
if 单行可以省略花括号
if (arg == null) return defaultValue;
Documentation
大写第一个字母,除非它是区分大小写的标识符。 以句号结尾(我想应该是“!”或“?”)。 所有注释都适用:文档注释,内联内容,甚至TODO。
使用块注释(/ * ... * /)暂时注释掉一段代码,但所有其他注释都应使用//
greet(name) {
// Assume we have a valid name.
print('Hi, $name!');
}
doc注释
出现在声明之前并使用dartdoc查找的特殊///语法的任何注释。
使用/// doc注释库,顶级变量,类型和成员。
//注释内部语句
/// The number of characters in this chunk when unsplit.
int get length => ...
考虑编写库级别的文档注释。
在Dart中,库本身就是用户可以直接使用,导入和思考的实体。这使得库指令成为文档的好地方,该文档向读者介绍其中提供的主要概念和功能。考虑包括:
库用途的单句摘要。整个库中使用的术语说明。使用API的几个完整的代码示例。链接到最重要或最常用的类和函数。链接到与库有关的域上的外部引用。
您可以通过在文件开头的库指令上方放置doc注释来对库进行记录。如果该库没有库指令,则可以添加一个,仅用于将文档注释挂起。
考虑为私有API和公共API编写文档注释。
文档注释不仅适用于库的公共API的外部使用者。它们也有助于理解从库的其他部分调用的私有成员。
务必以单句摘要开始文档注释。
以简短的,以用户为中心的描述(以句点结尾)开始您的文档注释。句子片段通常就足够了。为读者提供足够的背景信息以使其适应自己的方向,并决定他们是否应该继续阅读或寻找其他解决方案
文档注释第一句后空行
Dartdoc把第一句作为总结
/// Deletes the file at [path].
///
/// Throws an [IOError] if the file could not be found. Throws a
/// [PermissionError] if the file is present but could not be deleted.
void delete(String path) { ...}
类文档注释的读者可以清楚地看到该类的名称,其实现的接口等。在为成员阅读文档时,签名就在其中,并且包含的类很明显。 无需在文档注释中说明所有内容。 相反,请专注于说明读者尚不了解的内容。
类的文档注释通常是程序中最重要的文档。 他们描述了类型的结构,建立了所使用的术语,并为成员的其他文档注释提供了上下文。 此处的一些额外工作可以使所有其他成员更易于记录。
首选带有第三人称动词的起始函数或方法注释
/// Returns `true` if every element satisfies the [predicate].
bool all(bool predicate(T element)) => ...
/// Starts the stopwatch if not already running.
void start() { ...}
优先使用名词短语注释变量,getter或setter。
文档注释应强调该属性是什么。 即使对于可能进行计算或其他工作的getter也是如此。 调用者关心的是该工作的结果,而不是工作本身。
/// The current day of the week, where `0` is Sunday.
int weekday;
/// The number of checked buttons on the page.
int get checkedCount => ...
文档注释使用例子
/// Returns the lesser of two numbers.
///
/// ```dart
/// min(5, 3) == 3
/// ```
num min(num a, num b) => ...
在文档注释中使用方括号来引用范围内标识符。
/// Throws a [StateError] if ...
/// similar to [anotherMethod()], but ...
类的成员使用.
/// Similar to [Duration.inDays], but handles fractional days.
/// To create a point, call [Point()] or use [Point.polar()] to ...
参数的说明也使用[]
/// Defines a flag.
///
/// Throws an [ArgumentError] if there is already an option named [name] or
/// there is already an option using abbreviation [abbr]. Returns the new flag.
Flag addFlag(String name, String abbr) => ...
Markdown
将反引号用于代码块。
/// You can use [CodeBlockExample] like this:
///
/// ```
/// var example = CodeBlockExample();
/// print(example.isItGreat); // "Yes."
/// ```
更简洁。要清晰准确,但也要简洁。
避免缩写和首字母缩写,除非它们是显而易见的。许多人不知道“ i.e。”,“ e.g。”是什么 和“等”。 意思。 您确定该领域的每个人都知道的缩写可能不会像您想象的那样广为人知。
优选使用“ this”而不是“ the”来引用成员的实例。在为类记录成员时,通常需要参考该成员被调用的对象。 使用“ the”可能是模棱两可的。
Usage
库
库多个文件:使用URL
library my_library;
part "some/other/file.dart";
另一个文件
part of "../../my_library.dart";
不要导入另一个软件包的src目录中的库。
src目录为包私有的库
在您自己的软件包的lib目录中导入库时,请使用相对路径。不要使用package: URI.
lib外部的库永远不要使用相对导入来访问lib之下的库,反之亦然。
请遵循以下两个规则:
导入路径不应包含/ lib /。
lib下的库绝对不应使用../来转义lib目录。
Booleans
使用??转换值到 boolean
// If you want null to be false:
optionalThing?.isEnabled ?? false
;// If you want null to be true:
optionalThing?.isEnabled ?? true;
Strings
如果您有两个字符串文字(不是值,而是实际的带引号的文字形式),则无需使用+来串联它们。 就像在C和C ++中一样,只需将它们彼此相邻放置即可。 这是制作不适合一行的长字符串的好方法。
raiseAlarm( 'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
字符串插值而不是+
'Hello, $name! You are ${year - birth} years old.';
尽量避免在不需要时在插值中使用花括号。
'Hi, $name!'
"Wear your wildest $decade's outfit."
'Wear your wildest ${decade}s outfit.'
Collections
lists, maps, queues, and sets
有两种创建空的可增长list的方法:[]和List()。 同样,有三种方法可以制作空的map:{},Map()和LinkedHashMap()。如果要创建不可增长的列表或其他自定义集合类型,则一定要使用构造函数。 否则,请使用漂亮的文字语法。
var points = [];
var addresses = {};
var points =
var addresses =
不要使用.length来查看集合是否为空。使用.isEmpty and .isNotEmpty.
使用 .map(), .where()以及其他可迭代对象的方法代替for,来产生一个新集合
使用 for ...in 代替 forEach,如果是一个函数则使用forEach
people.forEach(print);
总是使用Map.forEach(),map不是可迭代对象
除非您打算更改结果的类型,否则请勿使用List.from()。
给定一个Iterable,有两种显而易见的方法来产生一个包含相同元素的新List:
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
// Creates a List
// Prints "List
print(List.from(iterable).runtimeType);
var ints = List
使用whereType进行类型过滤
var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType
对List
var ints = List
var reciprocals = stuff.map
cast()方法返回一个惰性集合,该集合检查每个操作上的元素类型。 如果只对几个元素执行几个操作,那么懒惰就可以了。 但是在许多情况下,延迟验证和包装的开销超过了好处。
替代方法并不总是有效,有时,cast()是正确的答案。
Variables
不要将变量显式初始化为null.
避免存储您可以计算的内容。
class Circle {
double radius;
Circle(this.radius);
double get area => pi * radius * radius;
double get circumference => pi * 2.0 * radius;
}
在某些情况下,您可能需要缓存慢速计算的结果,但是只有在知道性能问题后才这样做,请仔细进行处理,并在注释中说明优化方法。
Members
不要无必要地在getter和setter中包装字段。
编写代码的人似乎很喜欢=>,但是很容易滥用它并最终得到难以阅读的代码。如果您的声明多于两行或包含深层嵌套的表达式(级联和条件运算符是常见的违法者),请您自己和每个需要阅读代码的人都帮忙,并使用一个块体和一些语句。
不要使用this。 除了重定向到命名构造函数或避免命名冲突。
class Box {
var value;
void clear() { update(null); }
void update(value) { this.value = value; }
}
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// But now it will!
ShadeOfGray.alsoBlack() : this.black();
}
请注意,构造函数参数永远不会遮盖构造函数初始化列表中的字段:
class Box extends BaseBox {
var value;
Box(value) : value = value, super(value);
}
尽可能在声明时初始化字段。
如果字段不依赖于任何构造函数参数,则可以并且应该在其声明中对其进行初始化。 它花费更少的代码,并且确保如果类具有多个构造函数,您也不会忘记对其进行初始化。
Constructors
初始化形式,在Dart中,带有空主体的构造函数可以仅用分号终止。
class Point {
double x, y;
Point(this.x, this.y);
}
Dart 2将new关键字设为可选。不要使用
不要冗余使用const。
在表达式必须为常数的情况下,const关键字是隐式的,不需要编写也不需要编写。
Asynchrony
async 有用的情况包括:
您正在使用await。 (这是显而易见的。)
您正在异步返回错误。 throw 比return Future.error(...)短。
您正在返回一个值,并且希望将其隐式包装future对象。 async 比Future.value(...)短。
Future
print(await later);
}
Future
throw 'Error!';
}
Future
//future.then()
Future
return File(path).readAsString().then((contents) { return contents.contains('bear'); });
}
Future
var contents = await File(path).readAsString(); return contents.contains('bear');
}
Future
if (value is Future
var result = await value; print(result); return result;
} else {
print(value); return value as T;
}
}
Design
Names
使用一致的术语
避免缩写。除非缩写比未缩写的术语更普遍,否则不要缩写。 如果您缩写,请正确将其大写。
将描述性最强的名词放在最后。最后一个词应该是事物的最描述。 您可以在其前面加上其他词(例如形容词),以进一步描述事物。
pageCount // A count (of pages).
ConversionSink // A sink for doing conversions.
为非布尔属性或变量指定名词短语。如果用户更关心如何确定属性,则它可能应该是带有动词短语名称的方法。
list.length
context.lineWidth
quest.rampagingSwampBeast
为布尔属性或变量指定非命令式动词短语。好的名字往往以以下几种动词之一开头:
一种形式的“成为”:isEnabled,wasShown,willFire。 到目前为止,这些是最常见的。
辅助动词:hasElements,canClose,shouldConsume,mustSave。
一个活动动词:ignoresInput,writeFile。 这些是罕见的,因为它们通常是模棱两可的。 loggingResult是一个错误的名称,因为它可能表示“是否记录了结果”或“记录的结果”。 同样,closeingConnection可以是“连接是否正在关闭”或“连接正在关闭”。 当名称只能作为谓词读取时,允许使用主动动词。
将所有这些动词短语与方法名称区分开的原因在于它们不是强制性的。 布尔名称永远不会听起来像告诉对象执行某项操作的命令,因为访问属性不会更改对象。 (如果该属性确实以有意义的方式修改了该对象,则它应该是一个方法。)
isEmpty
hasElements
canClose
closesWindow
canShowPopup
hasShownPopup
考虑忽略动词的命名布尔参数。
对于布尔型的命名参数,名称通常很清楚,没有动词,并且代码在调用站点处读起来更好。
Isolate.spawn(entryPoint, message, paused: false);
var copy = List.from(elements, growable: true);
var regExp = RegExp(pattern, caseSensitive: false);
为布尔属性或变量指定“正”名称。
大多数布尔名称在概念上都具有“正”和“负”的形式,其中前者感觉像是基本概念,而后者则是其否定形式-“开放”和“封闭”,“启用”和“禁用”等。通常,后一种名称 从字面上看,其前缀否定了前者:“可见”和“不可见”,“连接”和“断开”,“零”和“非零”。
在选择真值代表的两种情况中的哪一种(以及因此为属性命名的情况)时,请选择肯定或更基本的一种。 布尔成员通常嵌套在逻辑表达式内,包括否定运算符。 如果您的属性本身读起来像一个否定词,那么读者就很难在思维上进行双重否定并理解代码的含义。
if (socket.isConnected && database.hasData) {
socket.write(database.read());
}
执行操作的函数或方法指定命令式动词短语
list.add("element");
queue.removeFirst();
window.refresh();
如果返回值是其主要目的,则应为函数或方法提供名词短语或非命令性动词短语。
var element = list.elementAt(3);
var first = list.firstWhere(test);
var char = string.codeUnitAt(4);
如果您想引起对函数或方法的注意,请考虑该函数或方法的命令性动词短语。
var table = database.downloadData();
var packageVersions = packageGraph.solveConstraints();
大多数情况下,请根据其为调用者所做的事情而不是其工作方式来命名您的成员。
避免使用get开头方法名称。
在大多数情况下,该方法应该是从名称中删除的getter方法。 例如,代替名为getBreakfastOrder()的方法,定义一个名为BreakfastOrder的方法。如果调用者最在乎方法返回的值,则只需删除get并使用名词短语名称(例如breakfastOrder())即可。如果调用者关心完成的工作,则使用动词短语名称,但选择比获得更准确地描述工作的动词,例如create, download, fetch, calculate, request, aggregate
如果将对象的状态复制到新对象,则将方法命名为to___()。
list.toSet();
stackTrace.toString();
如果方法返回由原始对象支持的其他表示形式,则将其命名为as___()。
var map = table.asMap();
var list = bytes.asFloat32List();
避免在函数或方法名称中描述参数。
list.add(element); 代替 list.addElement(element)
但是,提及一个参数以使其与采用不同类型的其他类似名称的方法进行歧义可能会很有用:
map.containsKey(key);
map.containsValue(value);
命名类型参数时,请遵循现有的助记符约定。
单个字母名称并不能完全说明问题,但几乎所有通用类型都使用它们。 幸运的是,他们大多以一致的助记符方式使用它们。 约定是:
E for the element type in a collection
K and V for the key and value types in an associative collection
R for a type used as the return type of a function or a class’s methods. This isn’t common, but appears in typedefs sometimes and in classes that implement the visitor pattern:
将T,S和U用于具有单个类型参数且周围类型使其含义显而易见的泛型。 这里有多个字母,以允许嵌套而不遮盖周围的名称。
Libraries
下划线字符(_)表示成员是其库的私有成员。这不只是约定俗成,而是内置在语言本身中。
考虑在同一个库中声明多个类。
如果一个库包含多个类,顶级变量和函数(如果它们在逻辑上都属于同一逻辑),完全可以。
将多个类一起放在一个库中可以启用一些有用的模式。由于Dart中的隐私权是在库级别而非类级别起作用的,因此这是一种定义“朋友”类的方法,就像在C ++中一样。在同一个库中声明的每个类都可以访问彼此的私有成员,但是该库外部的代码则不能。
Classes and mixins
避免定义一个成员抽象类,如果简单函数就可以实现功能
typedef Predicate
避免定义仅包含静态成员的类。
Dart具有顶级函数,变量和常量,因此您不需要仅用于定义某些内容的类。如果您想要的是名称空间,则最好使用库。库支持导入前缀和显示/隐藏。这些功能强大的工具可让您的代码使用者以最适合他们的方式处理名称冲突。
如果函数或变量在逻辑上未与类绑定,则将其放在顶层。如果您担心名称冲突,请给它一个更精确的名称,或者将其移到可以使用前缀导入的单独库中。
但是,这不是硬性规定。 对于常量和类似枚举的类型,将它们分组在一个类中是很自然的。
class Color {
static const red = '#f00';
static const green = '#0f0';
static const blue = '#00f';
static const black = '#000';
static const white = '#fff';
}
如果您的类支持扩展,请记录文档。
如果要允许类的子类,请说明。在类名称后加上Base,或在类文档注释中提及。
如果您的类支持用作接口,请记录文档。
如果该类没有文档注释或类似IterableMixin的明显名称,则假设您未使用mixin声明该类,则无法混入该类。
Constructors
如果类支持,请考虑使构造函数为const。
如果您有一个所有字段均为final的类,并且构造函数只对它们进行了初始化,则可以使该构造函数为const。 这样,用户可以在需要常量的地方(其他较大的常量,切换用例,默认参数值等)中创建类的实例。
但是请注意,const构造函数是您的公共API中的承诺。 如果以后将构造函数更改为非常量,则它将破坏用常量表达式调用它的用户。 如果您不想这样做,请不要将其设为const。 实际上,const构造函数对于简单,不可变的数据记录类最有用。
Members
in Dart; fields are “particularly fast getters”.
对于在概念上访问属性的操作,请使用getter:
操作不接受任何参数,并返回结果。
调用者最关心结果。如果要让调用者担心操作如何产生其结果而不是产生结果,请给该操作一个动词名称,该动词名称描述该工作并使其成为方法。这并不意味着操作必须特别快才能成为getter。 IterableBase.length为O(n),就可以了。getter进行大量计算很好。但是,如果它做了很多工作,您可能希望通过使其成为一种名称来描述其功能的动词的方法来引起他们的注意。
该操作没有用户可见的副作用。 访问实际字段不会更改对象或程序中的任何其他状态。 它不会产生输出,写入文件等。getter也不应执行这些操作。“用户可见”部分很重要。 getter可以修改隐藏状态或产生带外副作用,这是很好的选择。 getter可以懒惰地计算和存储其结果,写入缓存,记录日志等。只要调用者不关心副作用,就可以了。
该操作是幂等的。 “幂等”是一个奇怪的词,在此情况下,基本上意味着多次调用该操作每次都会产生相同的结果,除非在这些调用之间明确修改了某些状态。 (显然,如果在两次调用之间向列表中添加元素,list.length会产生不同的结果。)这里的“相同结果”并不意味着获取器必须在连续调用中从字面上产生相同的对象。 要求这样做将迫使许多getter进行脆性缓存,从而抵消了使用getter的全部要点。 每次使用getter返回新的future或列出新的future,这都是很正常的事情,而且非常好。 重要的是,future的完成价值相同,并且列表包含相同的元素。换句话说,结果值在调用者关心的方面应该相同。
生成的对象不会显示所有原始对象的状态。 字段仅显示一个对象。 如果您的操作返回的结果暴露了原始对象的整个状态,则最好使用to ___()或as ___()方法。
如果以上所有都描述了您的操作,那应该是一种getter。
对于在概念上更改属性的操作,请使用setter。
在setter和方法之间进行决定类似于在getter和方法之间进行决定。
该操作采用单个参数,并且不产生结果值。
该操作会更改对象中的某些状态。
该操作是幂等的。 就调用者而言,用相同的值两次调用相同的setter不应再次执行任何操作。 在内部,也许您有一些缓存失效或正在进行日志记录。 没关系。 但是从呼叫者的角度来看,第二个呼叫似乎没有任何作用。
不要在没有相应的getter的情况下定义setter。
避免从返回类型为bool,double,int或num的成员返回null。
如果您确实有这种类型的成员可能会返回null,请非常清楚地记录下来,包括将返回null的条件。
Types
如果代码带有类型注释,则该类型已明确写入代码中。
如果可以推断出代码,则不会编写任何类型注释,并且Dart会自行成功地找出类型。推论可能会失败,在这种情况下,准则不会认为推论是正确的。在某些地方,推理失败是一个静态错误。在其他情况下,Dart使用动态作为后备类型。
如果代码是动态的,则其静态类型是特殊的动态类型。可以将代码显式地注释为动态的,也可以对其进行推断。
如果类型不明显,则可以使用类型注释公共字段和顶级变量。
“显而易见”的情况,省略类型注释:
字面常量。
构造函数调用。
引用其他显式键入的常量。
关于数字和字符串的简单表达式。
读者应该熟悉的工厂方法,例如int.parse(),Future.wait()等。
如有疑问,请添加类型注释。 即使类型很明显,您仍可能希望显式注释。 如果推断的类型依赖于其他库的值或声明,则您可能需要键入注释的声明,以使对该其他库的更改不会在没有意识到的情况下默默更改您自己的API的类型。
如果类型不明显,请考虑类型注释私有字段和顶级变量。
公共声明上的类型注释可帮助您的代码用户。 私人成员的类型可以帮助维护者。 私有声明的范围较小,那些需要了解声明类型的人也更可能熟悉周围的代码。
局部变量,特别是在现代的代码中,函数往往很小,它们的作用域很小。 省略类型会使读者将注意力集中在更重要的变量名称及其初始值上。
for (var recipe in cookbook) {....
bool isValid(String value, bool Function(String) test) => ...
例外:有时,您需要一个表示多个不同函数类型的并集的类型。 例如,您可以接受带有一个参数的函数或带有两个参数的函数。 由于我们没有联合类型,因此无法精确键入该类型,因此通常必须使用动态。
setter总是在Dart中返回void。 写这个词是没有意义的。
typedef Comparison
直接定义函数类型,函数类型语法
Iterable
class FilteredObservable {
final bool Function(Event) _predicate;
final List
某些操作适用于任何可能的对象。 例如,log()方法可以接受任何对象并在其上调用toString()。 Dart中有两种类型允许所有值:Object和dynamic。 但是,它们传达了不同的东西。 如果只想声明允许所有对象,请像在Java或C#中那样使用Object。使用动态发送更复杂的信号。 这可能意味着Dart的类型系统不够复杂,无法表示允许的类型集,或者值来自互操作或静态类型系统的权限之外,或者您明确希望在运行时具有动态性。
Parameters
如果用户可能想省略较早的参数,请避免使用可选的位置参数。
可选的位置参数应具有逻辑顺序,以使较早的参数传递的频率比较晚的参数传递的频率高。 用户几乎永远不需要显式地传递一个“空洞”来忽略较早的位置参数而传递较晚的位置参数。 最好使用命名参数。
避免接受强制的参数,该参数接受特殊的“无参数”值。
如果用户在逻辑上省略了一个参数,则可以通过使该参数成为可选参数,而不是强制他们传递null,空字符串或其他表示“未传递”的特殊值,而让他们实际忽略该参数。
省略该参数会更简洁,并有助于防止在用户认为自己提供真实值时意外传递类似null的前哨值的错误。
不要使用包含开始和包含结束参数来接受范围。
如果要定义一个方法或函数,以使用户可以从某些整数索引的序列中选择一系列元素或项目,请获取一个开始索引,该索引指的是第一项,结束索引(可能是可选的)大于一个 最后一项的索引。
Equality
如果您覆盖==,请覆盖hashCode。
默认的哈希码实现提供了一个身份哈希-两个对象通常只有完全相同的对象才具有相同的哈希码。同样,==的默认行为是身份。
如果要覆盖==,则意味着您可能有不同的对象,这些对象被类视为“相等”。相等的任何两个对象必须具有相同的哈希码。否则,map和其他基于哈希的集合将无法识别这两个对象是等效的。
请确保您的==运算符服从相等的数学规则。
等价关系应为:
反身:a == a应该总是返回true。
对称:a == b应该返回与b == a相同的结果。
传递的:如果a == b和b == c都返回true,那么a == c也应该如此。
使用==的用户和代码希望遵守所有这些法律。如果您的班级不遵守这些规则,则==不是您要表达的操作的正确名称。
当定义==时,还必须定义hashCode。两者都应考虑对象的字段。如果这些字段发生更改,则表明该对象的哈希码可以更改。
大多数基于散列的集合都不希望这样-他们假设对象的散列代码永远是相同的,并且如果情况并非如此,则可能表现出不可预测的行为。
不要在自定义==运算符中检查null。
语言指定自动执行此检查,并且仅当右侧不为null时才调用==方法。
class Person {
final String name;
// ···
bool operator ==(other) => other is Person && name == other.name;
int get hashCode => name.hashCode;
}