文章目录
- 转载注明出处:https://blog.csdn.net/skysukai
- 1、编程风格指南
- 2、文档指南
- 2.1 注释
- 2.2 文档注释
- 2.2.1 使用/// 注释来记录成员和类型
- 2.2.2 为公共API编写文档注释
- 2.2.3 考虑编写库级文档注释
- 2.2.4 考虑为私有API编写文档注释
- 2.2.5 请用单句摘要开始文档注释
- 2.2.6 将文档注释的第一句分开到自己的段落中
- 2.2.7 避免与周围context的冗余
- 2.2.8 用第三人称动词启动函数或方法注释
- 2.2.9 用带有名词短语的变量、getter或setter开始注释
- 2.2.10 在起始库或用名词短语键入注释
- 2.2.11 考虑在文档注释中添加代码示例
- 2.2.12 请在文档注释中使用方括号来引用范围内标识符
- 2.2.13 使用方括号来解释参数,返回值和异常
- 2.2.14 请在元数据注释之前放置文档注释
- 2.3 Markdown
- 2.4 写代码
- 3、使用指南
- 4、设计指南
- 4.1 名称
- 4.1.1 命名形式上的一致性
- 4.1.2 避免使用缩写。如果使用,请正确地使用言简意赅的缩写。
- 4.1.3 建议把最具描述性的名词放在最后
- 4.1.4 考虑把代码写得像句子,而不带有歧义
- 4.1.5 首选非布尔属性或变量的名词短语
- 4.1.5 首选布尔属性或变量的非命令性动词短语
- 4.1.6 考虑使用省略命名布尔参数的动词
- 4.1.7 首选布尔属性或变量的“肯定的”名称
- 4.1.8 为有数据交互和改变的函数和方法选择一个命令性的动词短语
- 4.1.9 如果返回值是其主要目的,则为函数或方法首选名词短语或非命令性动词短语。
- 4.1.10 如果你在意函数或方法所执行的返回结果,请考虑命令性动词短语。
- 4.1.11 避免方法名称以``get``开头
- 4.1.12 如果方法是将对象复制到新对象,则将方法命名为to___()
- 4.1.13 如果方法返回的对象和原对象代表不同的含义,则将方法命名为as___()
- 4.1.15 在命名类型参数时,请遵循现有的助记符约定。
- 4.2 库
- 4.3 类和mixins
- 4.4 构造函数
- 4.5 成员
- 4.6 类型
- 4.6.1 如果类型不明显,则首选给公共字段和顶级变量添加类型注释
- 4.6.2 如果类型不明显,则首选给私有字段和顶级变量添加类型注释
- 4.6.3 ``避免类型注释初始化局部变量``
- 4.6.4 ``避免在函数表达式上添加可推断的参数类型``
- 4.6.5 ``避免泛型调用的冗余类型参数``
- 4.6.6 当Dart推断错误的类型时,请注释
- 4.6.7 首选使用泛型注释而不是让编译器推理失败
- 4.6.8 在函数类型注释中首选签名
- 4.6.9 请勿指定setter函数的返回类型
- 4.6.10 ``请勿使用旧式typedef语法``
- 4.6.11 在typedef上首选内联函数类型
- 4.6.12 考虑使用参数的函数类型语法
- 4.6.13 使用Object注释而不是dynamic类型来指示允许任何对象
- 4.6.14 ``请使用Future <void>作为不生成值的异步成员的返回类型``
- 4.6.15 避免使用FutureOr <T>作为返回类型
- 4.7 参数
- 4.8 相等
转载注明出处:https://blog.csdn.net/skysukai
本文摘抄翻译自Dart官方网站“Effective Dart”部分,选取比较常用的部分翻译出来,供大家参考。
原文将Dart高效编程分成了四个部分来给出建议:
1、编程风格指南(Style Guide)
2、文档指南(Documentation Guide)
3、使用指南(Usage Guide)
4、设计指南(Design Guide)
我也将按照这个顺序来摘抄翻译。
1、编程风格指南
1.1 标识符
标识符在Dart中有三种风格:
UpperCamelCase
大写每个单词的第一个字母,包括第一个字母
lowerCamelCase
名称大写每个单词的第一个字母,除了第一个单词总是小写,即使它是一个首字母缩写词
lowercase_with_underscores
只使用小写字母,即使是首字母缩略词,每个单词用_
分隔
1.1.1 使用UpperCamelCase来命名类型
1.1.2 使用lowercase_with_underscores命名库,包,目录和源文件
1.1.3 使用lowercase_with_underscores命名导入前缀
1.1.4 使用lowerCamelCase命名其他标识符
1.1.5 常量名称优先使用lowerCamelCase
1.1.6 首字母缩略词和缩写词比两个字母更长
大写的首字母缩略词可能难以阅读,而多个相邻的首字母缩略词可能会导致模糊的名称。 例如,如果名称以HTTPSFTP开头,则无法判断它是指HTTPS FTP还是HTTP SFTP。
为了避免这种情况,首字母缩略词和缩写词像普通词一样大写,除了两个字母的首字母缩略词。 (像ID和Mr.这样的两个字母缩写仍然像文字一样大写。)
比如:
HttpConnectionInfo
uiHandler
IOStream
HttpRequest
Id
DB
而不是:
HTTPConnection
UiHandler
IoStream
HTTPRequest
ID
Db
1.1.7 请勿对非私有标识符使用前置下划线
Dart在标识符中使用前导下划线将成员和顶级声明标记为私有。 这会训练开发者将前置下划线与其中一种声明相关联。 他们看到“ _ ”就会想到“private”。
局部变量,参数或库前缀没有“private”的概念。 当其中一个名称以下划线开头时,它会向读者发送一个令人困惑的信号。 为避免这种情况,请勿在这些名称中使用前置下划线。
1.1.8 不要使用前缀字母
匈牙利符号和其他方案出现在BCPL时,当编译器没有做太多工作帮助您理解代码。 因为Dart可以告诉您声明的类型,范围,可变性和其他属性,所以没有理由在标识符名称中对这些属性进行编码。
写成:
defaultTimeout
而不是:
kDefaultTimeout
1.2 顺序
1.2.1 带"dart"的导入放在所有导入之前
比如:
import ‘dart:async’;
import ‘dart:html’;
import ‘package:bar/bar.dart’;
import ‘package:foo/foo.dart’;
1.2.2 "external package"在其他导入之前
1.2.3 在所有导入后,请在单独的部分中指定导出
比如:
import ‘src/error.dart’;
import ‘src/foo_bar.dart’;
export ‘src/error.dart’;
1.2.4 按字母顺序分开排序导入的库
1.3 格式
像许多语言一样,Dart忽略了空格。 但是,人类没有。 具有一致的空格样式有助于确保人类读者以与编译器相同的方式查看代码。
1.3.1 使用dartfmt格式化代码
格式化是一项繁琐的工作,在重构过程中特别耗时。 幸运的是,你不必担心它。 我们提供了一个名为dartfmt的复杂自动代码格式化程序,它可以为您完成。 我们有一些关于它适用的规则的文档,但是Dart的官方空白处理规则是dartfmt产生的。
其余格式指南适用于dartfmt无法为您修复的一些内容。
1.3.2 考虑更改代码以使其更易于格式化
1.3.2 避免行超过80个字符
1.3.3 请对所有流控制语句使用花括号
2、文档指南
2.1 注释
2.1.1 注释应是一个句子
比如:
// Not if there is nothing before it.
if (_chunks.isEmpty) return false;
2.1.2 不要将块注释用于文档
写成:
greet(name) {
// Assume we have a valid name.
print(‘Hi, $name!’);
}
而不是:
greet(name) {
/* Assume we have a valid name. */
print(‘Hi, $name!’);
}
您可以使用块注释(/ * … * /)暂时注释掉一段代码,但所有其他注释应该使用//。
2.2 文档注释
Doc评论特别方便,因为dartdoc会解析它们并从中生成漂亮的doc页面。 doc注释是在声明之前出现的任何注释,并使用dartdoc查找///
的特殊语法。
2.2.1 使用/// 注释来记录成员和类型
比如:
/// The number of characters in this chunk when unsplit.
int get length => …
而不是:
// The number of characters in this chunk when unsplit.
int get length => …
2.2.2 为公共API编写文档注释
2.2.3 考虑编写库级文档注释
2.2.4 考虑为私有API编写文档注释
2.2.5 请用单句摘要开始文档注释
使用以句点结尾的简短,以用户为中心的描述开始您的doc注释。 句子片段通常就足够了。 为读者提供足够的上下文来定位这些注释,并决定是否应该继续阅读或寻找办法。
比如:
/// Deletes the file at [path] from the file system.
void delete(String path) {
…
}
而不是:
/// Depending on the state of the file system and the user’s permissions,
/// certain operations may or may not be possible. If there is no file at
/// [path] or it can’t be accessed, this function throws either [IOError]
/// or [PermissionError], respectively. Otherwise, this deletes the file.
void delete(String path) {
…
}
2.2.6 将文档注释的第一句分开到自己的段落中
在第一句之后添加一个空行,将其拆分为单独的段落。 如果多于一句解释是有用的,请将其余部分放在后面的段落中。
这有助于您编写一个简短的第一句话来总结文档。 此外,像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) {
…
}
而不是:
/// 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) {
…
}
2.2.7 避免与周围context的冗余
类的文档注释的读者可以清楚地看到类的名称,它实现的接口等。当读取成员的文档时,签名就在那里,而封闭的类是显而易见的。 这些都不需要在文档注释中拼写出来。 相反,专注于解释读者不知道的内容。
比如:
class RadioButtonWidget extends Widget {
/// Sets the tooltip to [lines], which should have been word wrapped using
/// the current font.
void tooltip(List lines) {
…
}
}
而不是:
class RadioButtonWidget extends Widget {
/// Sets the tooltip for this radio button widget to the list of strings in
/// [lines].
void tooltip(List lines) {
…
}
}
2.2.8 用第三人称动词启动函数或方法注释
2.2.9 用带有名词短语的变量、getter或setter开始注释
2.2.10 在起始库或用名词短语键入注释
2.2.11 考虑在文档注释中添加代码示例
比如:
/// Returns the lesser of two numbers.
///
/// dart
/// min(5, 3) == 3
///
num min(num a, num b) => …
2.2.12 请在文档注释中使用方括号来引用范围内标识符
2.2.13 使用方括号来解释参数,返回值和异常
其他语言使用详细标记和部分来描述方法的参数和返回值
/// Defines a flag with the given name and abbreviation.
///
/// @param name The name of the flag.
/// @param abbr The abbreviation for the flag.
/// @returns The new flag.
/// @throws ArgumentError If there is already an option with
/// the given name or abbreviation.
Flag addFlag(String name, String abbr) => …
Dart中的约定是将其集成到方法的描述中,并使用方括号突出显示参数。
/// 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) => …
2.2.14 请在元数据注释之前放置文档注释
比如:
/// A button that can be flipped on and off.
@Component(selector: ‘toggle’)
class ToggleComponent {}
而不是:
@Component(selector: ‘toggle’)
/// A button that can be flipped on and off.
class ToggleComponent {}
2.3 Markdown
您可以在文档注释中使用大多数markdown格式,dartdoc将使用markdown包相应地处理它。
有很多指南已经向您介绍Markdown。 它普遍受欢迎是我们选择它的原因。 这里只是一个简单的例子,让您了解所支持的内容:
/// This is a paragraph of regular text.
///
/// This sentence has two emphasized words (italics) and two
/// strong ones (bold).
///
/// A blank line creates a separate paragraph. It has some inline code
/// delimited using backticks.
///
/// * Unordered lists.
/// * Look like ASCII bullet lists.
/// * You can also use -
or +
.
///
/// 1. Numbered lists.
/// 2. Are, well, numbered.
/// 1. But the values don’t matter.
///
/// * You can nest lists too.
/// * They must be indented at least 4 spaces.
/// * (Well, 5 including the space after ///
.)
///
/// Code blocks are fenced in triple backticks:
///
/// /// this.code /// .will /// .retain(its, formatting); ///
///
/// The code language (for syntax highlighting) defaults to Dart. You can
/// specify it by putting the name of the language after the opening backticks:
///
/// html /// <h1>HTML is magical!</h1> ///
///
/// Links can be:
///
/// * http://www.just-a-bare-url.com
/// * with the URL inline
/// * [or separated out][ref link]
///
/// [ref link]: http://google.com
///
/// # A Header
///
/// ## A subheader
///
/// ### A subsubheader
///
/// #### If you need this many levels of headers, you’re doing it wrong
2.3.1 避免过度使用markdown
2.3.2 避免使用HTML进行格式化
2.3.3 首选代码块的反引号围栏
2.4 写代码
2.4.1 尽量简洁
2.4.2 避免缩写和首字母缩略词,除非它们是显而易见的
2.4.3 首先使用“this”而不是“the”来引用成员的实例
记录类的成员时,通常需要返回调用该成员的对象。 使用“the”可能含糊不清。
class Box {
/// The value this wraps.
var _value;
/// True if this box contains a value.
bool get hasValue => _value != null;
}
3、使用指南
3.1 库
3.1.1 使用part of指令
许多Dart开发人员完全避免使用part。 当每个库是单个文件时,他们发现更容易推理出他们的代码。 如果您确实选择使用part将库的一部分拆分到另一个文件中,Dart要求另一个文件依次指示它是哪个库的一部分。由于遗留原因,Dart允许指令的这一部分使用它所属的库的名称。 这使得工具更难以物理地查找主库文件,并且可能使部件实际上属于哪个库的模糊不清。
首选的现代语法是使用直接指向库文件的URI字符串,就像在其他指令中使用一样。 如果您有一些库,my_library.dart,其中包含:
library my_library;
part “some/other/file.dart”;
然后这部分文件应如下所示:
part of “…/…/my_library.dart”;
而不是:
part of my_library;
3.1.2 不要导入另一个包的src目录中的库
3.1.3 在您自己的包的lib目录中导入库时,首选相对路径
当从同一个包中的另一个库引用包的lib目录中的库时,相对URI或显式包:将起作用。
例如,假设你的目录结构如下:
my_package
└─ lib
├─ src
│ └─ utils.dart
└─ api.dart
如果api.dart想要导入utils.dart,它应该使用:
import ‘src/utils.dart’;
而不是:
import ‘package:my_package/src/utils.dart’;
没有深刻的理由偏爱前者 - 它只是更短,我们希望保持一致性。
“在您自己的包的lib目录中”部分很重要。 lib中的库可以导入lib(或其子目录)中的其他库。 lib之外的库可以使用相对导入来到lib之外的其他库。 遵循以下两条规则:
导入路径永远不应包含/ lib /。
lib下的库永远不应该使用…/来转义lib目录。
3.2 布尔型
3.2.1 请用??将null转换为布尔值
当表达式可以计算true,false或null时,此规则适用,并且您需要将结果传递给不接受null的内容。 常见的情况是将null-aware方法调用用作条件的结果:
if (optionalThing?.isEnabled) {
print(“Have enabled thing.”);
}
如果optionalThing为null,则此代码抛出异常。 要解决此问题,您需要将null值“转换”为true或false。 虽然您可以使用==执行此操作,但我们建议使用??:
// If you want null to be false:
optionalThing?.isEnabled ?? false;
// If you want null to be true:
optionalThing?.isEnabled ?? true;。
而不是:
// If you want null to be false:
optionalThing?.isEnabled == true;
// If you want null to be true:
optionalThing?.isEnabled != false;
这两个操作产生相同的结果并做正确的事情,但?? 是有三个主要原因的首选:
?? 运算符清楚地表明代码与空值有关。
= = true看起来像一个常见的新程序员错误,其中等式运算符是冗余的并且可以被删除。 当左边的布尔表达式不会产生null时是真的,但是当它不能产生时却不。
如果表达式为null,则?? 返回false和??返回true清楚地显示将使用的值。 使用= = true,您必须通过布尔逻辑来实现这意味着将null转换为false。
3.3 String
以下是在Dart中编写字符串时要记住的一些最佳实践
3.3.1 使用相邻的字符串来连接字符串文字
如果你有两个字符串文字 - 不是值,而是实际引用的文字形式 - 你不需要使用+来连接它们。 就像在C和C ++中一样,只需将它们放在一起就可以了。 这是制作多行的单个长字符串的好办法。
比如:
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other ’
‘parts are overrun by martians. Unclear which are which.’);
而不是:
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ’ +
‘parts are overrun by martians. Unclear which are which.’);
3.3.2 首选使用插值来组合字符串和值
如果您习惯其他语言,那么您习惯使用+的长链来构建文字和其他值的字符串。 这在Dart中有效,但使用插值几乎总是更清晰,更短:
比如:
‘Hello, $name! You are ${year - birth} years old.’;
而不是:
'Hello, ’ + name + ‘! You are ’ + (year - birth).toString() + ’ y…’;
3.3.3 避免在不需要时使用花括号进行插值
如果您要插入一个简单的标识符,而不是紧跟更多的字母数字文本,则应省略{}。
比如:
‘Hi, $name!’
“Wear your wildest $decade’s outfit.”
‘Wear your wildest ${decade}s outfit.’
而不是:
‘Hi, ${name}!’
“Wear your wildest ${decade}'s outfit.”
3.4 集合
开箱即用,Dart支持四种集合类型:列表,地图,队列和集合。 以下最佳实践适用于集合。
3.4.1 尽可能使用集合
有两种方法可以创建一个空的可扩展list:[]
和List()
。 同样,有三种方法可以创建一个空的linked hash map:{}
,Map()
和LinkedHashMap()
。
如果要创建不可扩展的列表或其他一些自定义集合类型,那么请务必使用构造函数。 核心库公开了那些构造函数以便于采用,但惯用的Dart代码不使用它们。
比如:
var points = [];
var addresses = {};
而不是:
var points = List();
var addresses = Map();
如果重要的话,你甚至可以为它们提供一个类型参数。
比如:
var points = <Point>[];
var addresses = <String, Address>{};
而不是:
var points = List();
var addresses = Map<String, Address>();
请注意,这条建议不适用于这些类的命名构造函数。 List.from(),Map.fromIterable()和相似的方法有他们自己的用法。 同样,如果您将大小传递给List()以创建不可增长的集合,那么使用它是有意义的。
3.4.2 不要使用.length来查看集合是否为空
Iterable规则不要求集合知道其长度或能够在恒定时间内提供它。 调用.length只是为了查看集合是否包含任何内容可能会非常缓慢。
相反,有更快,更可读的getter:.isEmpty和.isNotEmpty。 使用不需要绑定结果。
比如:
if (lunchBox.isEmpty) return ‘so hungry…’;
if (words.isNotEmpty) return words.join(’ ‘);
而不是:
if (lunchBox.length == 0) return ‘so hungry…’;
if (!words.isEmpty) return words.join(’ ');
3.4.3 考虑使用高阶方法转换序列
如果你有一个集合并想从中生成一个新的修改过的集合,那么使用.map(),. where()以及Iterable上的其他方便方法通常更短,更具说明性。
使用这些而不是命令循环表明你的意图是产生一个新序列而不产生副作用。
比如:
var aquaticNames = animals
.where((animal) => animal.isAquatic)
.map((animal) => animal.name);
与此同时,这可能会走得太远。 如果您正在链接或嵌套许多高阶方法,那么编写一大块命令性代码可能会更加清晰。
3.4.4 使用带有函数文字的Iterable.forEach()
forEach()函数在JavaScript中被广泛使用,因为内置的for-in循环不能达到你通常想要的效果。 在Dart中,如果要迭代序列,那么惯用的方法就是使用循环。
比如:
for (var person in people) {
…
}
而不是:
people.forEach((person) {
…
});
请注意,这条建议特别指出“函数文字”。 如果要在每个元素上调用一些已存在的函数,forEach()就可以了。
people.forEach(print);
另请注意,使用Map.forEach()始终可以使用。 Maps不可iterable,因此本条建议不适用。
3.4.5 除非您打算更改结果的类型,否则请勿使用List.from()
给定Iterable,有两种显而易见的方法可以生成包含相同元素的新List:
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
明显的区别是第一个更短。 重要的区别是第一个保留了原始对象的类型参数:
比如:
// Creates a List:
var iterable = [1, 2, 3];
// Prints “List”:
print(iterable.toList().runtimeType);
而不是:
// Creates a List:
var iterable = [1, 2, 3];
// Prints “List”:
print(List.from(iterable).runtimeType);
如果要更改类型,则调用List.from()非常有用:
var numbers = [1, 2.3, 4]; // List.
numbers.removeAt(1); // Now it only contains integers.
var ints = List.from(numbers);
但是如果你的目标只是复制iterable并保留其原始类型,或者你不关心类型,那么使用toList()。
3.4.6 请使用whereType()按类型过滤集合
假设您有一个包含对象混合的列表,并且您希望只获取整数。 你可以像这样使用where():
var objects = [1, “a”, 2, “b”, 3];
var ints = objects.where((e) => e is int);
这是冗长的,但更糟糕的是,它返回一个可能不是你想要的类型的iterable。 在这里的示例中,它返回一个Iterable <Object>,即使您可能想要一个Iterable <int>,因为那是您要过滤它的类型。
有时您会看到通过添加cast()来“纠正”上述错误的代码:
var objects = [1, “a”, 2, “b”, 3];
var ints = objects.where((e) => e is int).cast<int>();
这是冗长的,并导致创建两个包装器,具有两层间接和冗余运行时检查。 幸运的是,核心库具有针对此确切用例的whereType()方法:
var objects = [1, “a”, 2, “b”, 3];
var ints = objects.whereType<int>();
使用whereType()是简洁的,产生所需类型的Iterable,并且没有不必要的包装。
3.4.7 当附近的操作有cast转换时,请勿使用cast()
通常,当您处理可迭代或流时,您可以对其执行多次转换。 最后,您希望生成具有特定类型参数的对象。 而不是调用cast(),看看是否有一个现有的转换可以改变类型。
如果您已经调用了toList(),请将其替换为对List <T> .from()的调用,其中T是您想要的结果列表的类型。
比如:
var stuff = <dynamic>[1, 2];
var ints = List.from(stuff);
而不是:
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();
如果要调用map(),请为其指定一个显式类型参数,以便生成所需类型的可迭代。 类型推断通常会根据您传递给map()的函数为您选择正确的类型,但有时您需要明确。
比如:
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
而不是:
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();;
3.4.8 避免使用cast()
这是对先前规则的更软件的概括。 有时候没有附近的操作可以用来修复某些对象的类型。 即便如此,尽可能避免使用cast()来“改变”集合的类型。
请改为选择以下任何选项:
使用正确的类型创建集合。 更改首次创建集合的代码,以使其具有正确的类型。
在访问时强制转换元素。 如果您立即迭代集合,则在迭代内部转换每个元素。
强烈推荐使用List.from()。 如果您最终将访问集合中的大多数元素,并且您不需要该对象转换到原始对象,请使用List.from()进行转换。
cast()方法返回一个惰性集合,用于检查每个操作的元素类型。 如果只对少数元素执行少量操作,那么懒惰就会很好。 但在许多情况下,延迟验证和包装的开销超过了好处。
以下是使用正确类型创建它的示例:
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
而不是:
List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}
访问时转换每个元素:
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (var n in objects) {
if ((n as int).isEven) print(n);
}
}
而不是:
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (var n in objects.cast<int>()) {
if (n.isEven) print(n);
}
}
使用List.from():
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}
而不是:
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = objects.cast<int>();
ints.sort();
return ints[ints.length ~/ 2];
}
当然,这些替代方案并不总是有效,有时cast()是正确的答案。 但是考虑到这种方法有点危险和不可取 - 它可能很慢,如果你不小心,可能会在运行时失败。
3.5 函数
在Dart中,甚至函数都是对象。 以下是一些涉及函数的最佳实践。
3.5.1 使用函数声明将函数绑定到名称
现代语言已经意识到本地嵌套函数和闭包是多么有用。 在另一个函数中定义一个函数是很常见的。 在许多情况下,此函数立即用作回调,不需要名称。 函数表达式非常适合。
但是,如果您确实需要为其命名,请使用函数声明语句,而不是将lambda绑定到变量。
比如:
void main() {
localFunction() {
…
}
}
而不是:
void main() {
var localFunction = () {
…
};
}
3.5.2 tear-off时不要创建一个lambda
如果你引用一个对象上的方法但省略了括号,Dart会给你一个“tear-off” - 一个闭包,它接受与方法相同的参数,并在你调用它时调用它。
如果您有一个调用方法的函数,该函数具有与传递给它的参数相同的参数,则无需手动将调用包装在lambda中。
比如:
names.forEach(print);
而不是:
names.forEach((name) {
print(name);
});
3.6 参数
3.6.1 请使用=将命名参数与其默认值分开
由于遗留原因,Dart允许:和=作为命名参数的默认值分隔符。 为了与可选的位置参数保持一致,请使用=。
比如:
void insert(Object item, {int at = 0}) { … }
而不是:
void insert(Object item, {int at: 0}) { … }
3.6.2 请勿使用显式默认值null
如果您将参数设置为可选但不提供默认值,则该语言隐式使用null作为默认值,因此无需编写它。
3.7 变量
以下最佳实践描述了如何在Dart中最好地使用变量。
3.7.1 不要将变量显式初始化为null
在Dart中,未自动显式初始化的变量或字段将初始化为null。 这是由语言可靠地指定的。 Dart中没有“未初始化记忆”的概念。 添加= null是多余的,不需要。
3.7.2 避免存储您可以计算的内容
在设计类时,您经常希望将多个视图暴露给相同的基础状态。 通常,您会看到在构造函数中计算所有这些视图的代码,然后存储它们:
class Circle {
num radius;
num area;
num circumference;
Circle(num radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
这段代码有两个错误。 首先,它可能会浪费内存。 严格来说,area和circumference是高速缓存。 它们是存储的计算结果,我们可以从我们已有的其他数据重新计算。这两个参数正在增加内存同时降低CPU使用率。 这是否是一个值得权衡的性能问题?
更糟糕的是,代码是错误的。 缓存的问题是无效 - 您如何知道缓存何时过期并需要重新计算? 在这里,我们永远不会这样做,即使radius是可变的。 您可以指定不同的值,area和circumference将保留其先前的、现在不正确的值。
要正确处理缓存失效,我们需要这样做:
class Circle {
num _radius;
num get radius => _radius;
set radius(num value) {
_radius = value;
_recalculate();
}
num _area;
num get area => _area;
num _circumference;
num get circumference => _circumference;
Circle(this._radius) {
_recalculate();
}
void _recalculate() {
_area = pi * _radius * _radius;
_circumference = pi * 2.0 * _radius;
}
}
这是编写,维护,调试和读取的大量代码。 相反,您的第一个实现应该是:
class Circle {
num radius;
Circle(this.radius);
num get area => pi * radius * radius;
num get circumference => pi * 2.0 * radius;
}
此代码更短,使用更少的内存,并且更不容易出错。 它存储表示圆所需的最少量数据。 没有字段可以不同步,因为只有一个来源。
在某些情况下,您可能需要缓存慢速计算的结果,但只有在您知道性能问题后才能执行此操作,请仔细执行,并留下解释优化的注释。
3.8 成员
在Dart中,对象具有可以是函数(方法)或数据(实例变量)的成员。 以下最佳实践适用于对象的成员。
3.8.1 不要在不必要的情况下将字段包裹在getter和setter中。
在Java和C#中,通常把字段隐藏getter和setter(或C#中)后面,即使实现需要用到字段。 这样,即使这些字段不会被用到,就要产生更多的代码。 在java中使用getter和直接访问字段是不同的,甚至在C#访问属性与访问原始字段不是二进制兼容的。
Dart没有这个限制。 字段和getter / setter是完全区分开的。 您可以在类中公开一个字段,然后将其包装在getter和setter中,而不必有其他任何使用该字段的代码。
比如:
class Box {
var contents;
}
而不是:
class Box {
var _contents;
get contents => _contents;
set contents(value) {
_contents = value;
}
}
3.8.2 首选使用final字段创建只读属性
如果您有一个外部代码应该能够看到但不能分配的字段,那么在许多情况下有效的简单解决方案就是将其标记为final。
3.8.3 考虑使用=>表示简单成员
除了使用=>作为函数表达式之外,Dart还允许您使用它来定义成员。 该样式非常适合仅计算和返回值的简单成员。
double get area => (right - left) * (bottom - top);
bool isReady(num time) => minTime == null || minTime <= time;
String capitalize(String name) =>
‘
n
a
m
e
[
0
]
.
t
o
U
p
p
e
r
C
a
s
e
(
)
{name[0].toUpperCase()}
name[0].toUpperCase(){name.substring(1)}’;
编写代码的人似乎喜欢=>,但是很容易滥用它并最终得到难以阅读的代码。 如果您的声明超过几行或包含深层嵌套的表达式 - 级联和条件运算符是常见的错误 - 请大家使用块体和一些表达式。
您还可以对不返回值的成员使用=>。 通常,当setter很小并且具有使用=>的相应getter时。
num get x => center.x;
set x(num value) => center = Point(value, center.y);
3.8.4 不要使用this. 除了重定向到命名构造函数或避免阴影
JavaScript需要明确这一点。 引用当前正在执行其方法的对象上的成员,但类似Dart的C ++,Java和C#没有这个限制。
3.8.5 尽可能在声明中初始化字段
如果某个字段不依赖于任何构造函数参数,则可以并且应该在其声明中对其进行初始化。 它需要更少的代码,并确保如果类有多个构造函数,不要忘记初始化字段。
3.9 构造函数
3.9.1 尽可能使用初始化形式
许多字段直接从构造函数参数初始化,如:
class Point {
num x, y;
Point(num x, num y) {
this.x = x;
this.y = y;
}
}
我们必须在这里输入x四次定义一个字段。 我们可以做得更好:
class Point {
num x, y;
Point(this.x, this.y);
}
这里this. 构造函数参数之前的语法称为“初始化形式”。 你不能总是利用它。 有时你希望有一个命名参数,其名称与您正在初始化的字段的名称不匹配。 但是当你可以使用初始化形式时,你应该使用它。
3.9.2 初始化不需要参数类型
如果构造函数参数正在使用this.初始化字段,那么参数的类型应理解为与字段相同的类型。
比如:
class Point {
int x, y;
Point(this.x, this.y);
}
而不是:
class Point {
int x, y;
Point(int this.x, int this.y);
}
3.9.3 对于空构造函数体请使用; 而不是{}
3.9.4 别使用new
Dart 2使new关键字可选。 即使在Dart 1中,它的含义也从未被清楚,因为工厂构造函数意味着新的调用可能仍然不会实际返回一个新对象。
但请考虑将其弃用并从代码中删除它。
比如:
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(
child: Text(‘Increment’),
),
Text(‘Click!’),
],
);
}
而不是:
Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(
child: new Text(‘Increment’),
),
new Text(‘Click!’),
],
);
}
3.9.5 不要冗余地使用const
在表达式必须是常量的上下文中,const关键字是隐式的,不需要写,也不应该。 这些背景是内在的任何表达:
const集合文字。
一个const构造函数调用 元数据注释。
const变量声明的初始化器。
一个switch case表达式 - 紧接在case之后的部分:而不是case的主体。
基本上,任何写入new而不是const的错误的地方,Dart 2允许你省略const.
比如:
const primaryColors = [
Color(“red”, [255, 0, 0]),
Color(“green”, [0, 255, 0]),
Color(“blue”, [0, 0, 255]),
];
而不是:
const primaryColors = const [
const Color(“red”, const [255, 0, 0]),
const Color(“green”, const [0, 255, 0]),
const Color(“blue”, const [0, 0, 255]),
];
3.10 出错处理
3.10.1 避免没有on语句catches
3.10.2 如果没有on子句,请勿丢弃来自catches捕获的错误
3.10.3 抛出仅针对程序错误实现Error的对象
3.10.4 不要显式地捕获Error或它的实现类型
3.10.5 请使用rethrow重新抛出捕获的异常
3.11 异步
3.11.1 在futures上使用async/await
众所周知,异步代码很难读取和调试。 async / await语法提高了可读性,并允许您使用异步代码中的所有Dart控制流结构。
3.11.2 如果没有有用的效果,请不要使用异步
很容易养成在任何与异步相关的函数上使用异步的习惯。 但在某些情况下,它是无关紧要的。 如果可以在不更改函数行为的情况下省略异步,请执行此操作。
比如:
Future afterTwoThings(Future first, Future second) {
return Future.wait([first, second]);
}
而不是:
Future afterTwoThings(Future first, Future second) async {
return Future.wait([first, second]);
}
异步有用的情况包括:
你正在使用await。 (这是显而易见的。)
您将异步返回错误。 async然后throw比返回Future.error(...)更短。
您正在返回一个值,并且您希望将来隐式包装它。 async比Future.value(...)短。
Future usesAwait(Future later) async {
print(await later);
}
Future asyncError() async {
throw ‘Error!’;
}
Future asyncValue() async => ‘value’;
3.11.3 考虑使用高阶方法转换流
这与上述关于迭代的建议相似。 Streams支持许多相同的方法,并且还可以正确处理传输错误,关闭等操作。
3.11.4 避免直接使用Completer
许多刚接触异步编程的人想要编写可以产生future的代码。 Future中的构造函数似乎不符合他们的需要,因此他们最终找到了Completer类并使用它。
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains(‘bear’));
});
return completer.future;
}
两种低级代码需要Completer:新的异步原语,以及与不使用future的异步代码的接口。 大多数其他代码应该使用async / await或Future.then(),因为它们更清晰并且使错误处理更容易。
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains(‘bear’);
});
}
Future<bool> fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains(‘bear’);
}
3.11.5 当消除其类型参数可能是Object的FutureOr <T>时,测试Future <T>
在使用FutureOr <T>执行任何有用的操作之前,通常需要检查是否有Future <T>或裸T.如果type参数是某个特定类型,如FutureOr <int> ,使用哪个测试无关紧要,是int还是Future <int>。 两者都有效,因为这两种类型是不相交的。
但是,如果值类型是Object或可能使用Object实例化的类型参数,则两个分支重叠。 Future <Object>本身实现了Object,因此是Object或者是T,其中T是一个可以用Object实例化的类型参数,即使对象是未来,它也会返回true。 相反,明确测试Future案例:
Future<T> logValue<T>(FutureOr<T> value) async {
if (value is Future<T>) {
var result = await value;
print(result);
return result;
} else {
print(value);
return value as T;
}
}
而不是:
Future<T> logValue<T>(FutureOr<T> value) async {
if (value is T) {
print(value);
return value;
} else {
var result = await value;
print(result);
return result;
}
}
4、设计指南
这是最软件,但确范围最广的指南。包括了设计一致性、dart库中可使用的API,同时也包括了签名和申明的指南。
4.1 名称
// If you want null to be false:
optionalThing?.isEnabled == true;
// If you want null to be true:
optionalThing?.isEnabled != false;
4.1.1 命名形式上的一致性
4.1.2 避免使用缩写。如果使用,请正确地使用言简意赅的缩写。
4.1.3 建议把最具描述性的名词放在最后
4.1.4 考虑把代码写得像句子,而不带有歧义
4.1.5 首选非布尔属性或变量的名词短语
4.1.5 首选布尔属性或变量的非命令性动词短语
4.1.6 考虑使用省略命名布尔参数的动词
4.1.7 首选布尔属性或变量的“肯定的”名称
4.1.8 为有数据交互和改变的函数和方法选择一个命令性的动词短语
4.1.9 如果返回值是其主要目的,则为函数或方法首选名词短语或非命令性动词短语。
4.1.10 如果你在意函数或方法所执行的返回结果,请考虑命令性动词短语。
4.1.11 避免方法名称以get
开头
在多数情况下,get
应该从getter函数中移除。例如:以breakfastOrder
来取代getBreakfastOrder()
。
即使成员变量有提炼成getter
方法的必要,你也应该避免使用getter
。
4.1.12 如果方法是将对象复制到新对象,则将方法命名为to___()
list.toSet();
stackTrace.toString();
dateTime.toLocal();
4.1.13 如果方法返回的对象和原对象代表不同的含义,则将方法命名为as___()
var map = table.asMap();
var list = bytes.asFloat32List();
var future = subscription.asFuture();
4.1.14 避免使用函数或方法名称中的名称来描述一个形参。
如:
list.add(element);
map.remove(key);
而不是:
list.addElement(element);
map.removeKey(key);
也有例外,当用以消除有相似名称但不同类型参数的歧义时:
map.containsKey(key);
map.containsValue(value);
4.1.15 在命名类型参数时,请遵循现有的助记符约定。
E表示集合中的元素类型:
class IterableBase<E> {}
class List<E> {}
class HashSet<E> {}
K和V表示关联集合中的键和值类型:
class Map<K, V> {}
class Multimap<K, V> {}
class MapEntry<K, V> {}
R表示用作函数的返回类型或类的方法的类型。这种情况并不常见,但有时会出现在typedef中,也会出现在实现访问者模式的类中。
4.2 库
4.2.1 首选将声明设为私有
4.2.2 考虑将多个类放在一个库里
4.3 类和mixins
4.3.1 *避免简单函数可以完成功能时定义只有一个成员的抽象类
(这条建议特别重要)
与Java不同,Dart具有一流的函数,闭包以及使用它们的轻松语法。如果您只需要回调,只需使用一个函数即可。如果您正在定义一个类,并且它只有一个具有无意义名称的抽象成员,如call或invoke,那么您很有可能只需要一个函数。
如:
typedef Predicate<E> = bool Function(E element);
而不是:
abstract class Predicate<E> {
bool test(E element);
}
4.3.2 避免定义只有静态成员变量的类
在Java和C#中,每个定义都必须在一个类中,因此通常会看到只存在静态成员变量的“类”。其他类用作命名空间 - 一种为一堆成员提供共享前缀以使它们彼此关联或避免名称冲突的方法。
Dart具有顶级函数,变量和常量,因此您不需要一个类来定义某些东西。如果你只需要一个命名空间,库可能更为合适。库支持import
和show/hide
的组合使用。这些是强大的工具,让代码的使用者以最适合他们的方式处理名称冲突。
如果函数或变量在逻辑上与类无关,请将其置于顶层。如果您担心名称冲突,请为其指定更精确的名称,或将其移动到可以使用import
前缀导入的单独库中。
如:
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’;
}
4.3.3 避免继承不打算进行子类化的类
4.3.4 如果你的类可以被继承增加一些文档
4.3.5 避免实现一个不打算作为接口的类
4.3.6 增加文档,如果您的类可以用作接口
4.3.7 请使用mixin来定义mixin类型
Dart最初没有单独的语法来声明一个类要混合到其他类中。相反,任何满足某些限制的类(没有非默认构造函数,没有超类等)都可以用mixin类型。因为类的作者可能并不打算将它混合到其他类。
Dart 2.1.0添加了一个mixin关键字,用于明确声明mixin。使用它创建的类型只能用作mixins,并且该语言还可以确保您的mixin保持在限制范围内。 在定义要用作mixin的新类型时,请使用此语法。
mixin ClickableMixin implements Control {
bool _isDown = false;
void click();
void mouseDown() {
_isDown = true;
}
void mouseUp() {
if (_isDown) click();
_isDown = false;
}
}
您可能仍会遇到使用类来定义mixins的旧代码,但首选新语法。
4.3.8 避免mixin一个不打算作为mixin的类型
4.4 构造函数
4.4.1 如果类支持请考虑使用const修饰构造函数
4.5 成员
4.5.1 首选将字段和顶级变量设为final
4.5.2 请使用getter进行概念上访问属性的操作
4.5.3 请使用setter进行概念上改变属性的操作
4.5.4 不要在没有相应getter的情况下定义setter
4.5.5 避免从返回类型为bool,double,int或num的成员返回null
4.5.6 避免从方法中返回this只是为了启用流畅的界面
方法级联是链式方法调用的更好解决方案
如:
var buffer = StringBuffer()
. .write(‘one’)
. .write(‘two’)
. .write(‘three’);
而不是:
var buffer = StringBuffer()
.write(‘one’)
.write(‘two’)
.write(‘three’);
4.6 类型
4.6.1 如果类型不明显,则首选给公共字段和顶级变量添加类型注释
比如不是:
install(id, destination) => …
而是:
Future install(PackageId id, String destination) => …
但在某些情况下,类型是如此明显,以至于编写类型注释是毫无意义的:
const screenWidth = 640; // Inferred as int.
4.6.2 如果类型不明显,则首选给私有字段和顶级变量添加类型注释
4.6.3 避免类型注释初始化局部变量
局部变量,特别是在函数往往很小的现代代码中,范围很小。省略该类型会将读者的注意力集中在变量更重要的名称及其初始化值上。
比如:
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
var desserts = <List<Ingredient>>[];
for (var 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 (List<Ingredient> recipe in cookbook) {
if (pantry.containsAll(recipe)) {
desserts.add(recipe);
}
}
return desserts;
}
如果局部变量没有初始值设定项,则无法推断其类型。 在这种情况下,注释是个好主意。 否则,您将还有泛型可用并失去静态类型检查的好处。
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
4.6.4 避免在函数表达式上添加可推断的参数类型
匿名函数几乎总是立即参数给传递采用某种类型回调的方法。(如果函数没有立即使用,通常得将它作为命名声明。)在类型化context中创建函数表达式时,Dart会尝试根据预期类型推断函数的参数类型。
例如,当您将函数表达式传递给Iterable.map()时,将根据map()期望的回调类型推断函数的参数类型:
var names = people.map((person) => person .name);
而不是:
var names = people.map((Person person) => person .name);
在极少数情况下,context环境不够精确,无法为一个或多个函数的参数提供类型。 在这些情况下,您可能需要注释。
4.6.5 避免泛型调用的冗余类型参数
如果上下文推理将填充类型参数,则手动写入类型参数是多余的。 如果调用的是注释变量的初始化程序,或者是函数的参数,那么上下文推理通常会为您填写类型:
比如:
Set<String> things = Set();
而不是:
Set<String> things = Set<String>();
这里,变量的类型注释用于在初始化程序中推断构造函数调用的类型参数。
在其他情况下,没有足够的信息来推断类型,然后你应该写类型参数:
var things = Set<String>();
而不是:
var things = Set();
这里,由于变量没有类型注释,因此没有足够的上下文来确定要创建的Set类型,因此应该显式提供type参数。
4.6.6 当Dart推断错误的类型时,请注释
有时,Dart推断出一种类型,但不是你想要的类型。 例如,您可能希望变量的类型是初始化程序类型的超类型,以便稍后可以为变量分配一些其他同级类型:
比如:
num highScore(List<num> scores) {
num highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
而不是:
num highScore(List<num> scores) {
var highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
这里,如果分数包含双精度,如[1.2],那么最高分配将失败,因为它的推断类型是int,而不是num。 在这些情况下,显式注释是有意义的。
4.6.7 首选使用泛型注释而不是让编译器推理失败
Dart允许您在许多地方省略类型注释,并尝试为您推断类型。 在某些情况下,如果推理失败,它会默默地为您提供dynamic类型。 如果dynamic类型是你想要的类型,这在技术上是最简洁的方式。
然而,这不是最简洁的方式。 如果读者看到注释缺失的代码无法知道您是否希望它是dynamic类型的以期编译器推断以填充其类型,或者只是忘记编写注释。
当dynamic是您想要的类型时,明确地编写它会使您的意图清晰。
比如:
dynamic mergeJson(dynamic original, dynamic changes) => …
而不是:
mergeJson(original, changes) => …
4.6.8 在函数类型注释中首选签名
标识符函数本身没有任何返回类型或参数签名涉及到固定的函数类型。 这种类型仅比使用dynamic类型更有用。 如果要进行注释,请选择包含函数参数和返回类型的完整函数类型。
比如:
bool isValid(String value, bool Function(String) test) => …
而不是:
bool isValid(String value, Function test) => …
例外:有时,您需要一个表示多个不同函数类型多个类型。 例如,您可以接受带有一个参数的函数或带有两个参数的函数。 由于我们没有union类型,因此没有办法精确地键入它,您通常必须使用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.”);
}
}
}
4.6.9 请勿指定setter函数的返回类型
Setter总是在Dart中返回void。 写这个词毫无意义。
比如:
set foo(Foo value) { … }
而不是:
void set foo(Foo value) { … }
4.6.10 请勿使用旧式typedef语法
Dart有两个用于为函数类型定义命名typedef的符号。 原始语法如下:
typedef int Comparisont<T>(T a, T b);
这个语法有许多问题:
无法为泛型函数类型指定名称。 在上面的示例中,typedef本身是泛型。 如果在代码中引用Comparison而没有类型参数,则隐式获取函数类型int Function(dynamic,dynamic),而不是int Function<T>(T,T)。 这在实践中并不常见,但在某些极端情况下很重要。
参数中的单个标识符被解释为参数的名称,而不是其类型。比如:
typedef 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中=之后的相同函数类型语法也允许在类型注释可能出现的任何地方,这为我们提供了一种在程序中的任何位置编写函数类型的一致方法。
Dart仍然支持旧的typedef语法以避免破坏现有代码,但它已被弃用。
4.6.11 在typedef上首选内联函数类型
在Dart 1中,如果要为字段,变量或泛型类型参数使用函数类型,则必须首先为其定义typedef。 Dart 2支持函数类型语法,可以在允许类型注释的任何地方使用它:
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 (var observer in _observers) {
observer(event);
last = observer;
}
return last;
}
}
如果函数类型特别长或经常使用,可能仍然值得定义typedef。 但在大多数情况下,用户希望看到函数类型实际上在哪里使用,并且函数类型语法使它们清晰。
4.6.12 考虑使用参数的函数类型语法
在定义类型为函数的参数时,Dart具有特殊语法。 与C类似,您可以使用函数的返回类型和参数签名来包围参数的名称:
Iterable <T> where(bool predicate(T element)) => …
在Dart 2添加函数类型语法之前,这是在不定义typedef的情况下为参数提供函数类型的唯一方法。 既然Dart有函数类型的通用符号,你也可以将它用于函数类型的参数:
Iterable<T> where(bool Function(T) predicate) => …
新语法稍微冗长一点,但这样和其他使用新语法的地方保持了一致性。
4.6.13 使用Object注释而不是dynamic类型来指示允许任何对象
某些操作适用于任何可能的对象。 例如,log()方法可以接受任何对象并在其上调用toString()。 Dart中的两种类型允许所有值:Object和dynamic类型。 但是,他们传达了不同的东西。 如果您只想声明允许所有对象,请使用Object,就像在Java或C#中一样。
使用dynamic发送更复杂的信号。 这可能意味着Dart的类型系统不够复杂,无法表示允许的类型集,或者值来自互操作或者在静态类型系统范围之外,或者您明确希望运行时dynamic在 程序中的那一点。
void log(Object object) {
print(object.toString());
}
/// Returns a Boolean representation for [arg], which must
/// be a String or bool.
bool convertToBool(dynamic arg) {
if (arg is bool) return arg;
if (arg is String) return arg == ‘true’;
throw ArgumentError(‘Cannot convert $arg to a bool.’);
}
4.6.14 请使用Future <void>作为不生成值的异步成员的返回类型
如果您有一个不返回值的同步函数,则使用void作为返回类型。 对于不生成值但是调用者可能需要等待的方法的异步等价物是Future <void>。
您可能会看到使用Future或Future 的代码,因为旧版本的Dart不允许void作为类型参数。 既然如此,你应该使用它。 这样做更直接地匹配您键入类似同步函数的方式,并为调用者和函数体提供更好的错误检查。
对于不返回有用值的异步函数以及没有调用者需要等待异步工作或处理异步失败的异步函数,请使用返回类型的void。
4.6.15 避免使用FutureOr <T>作为返回类型
如果一个方法接受一个FutureOr <int>,那么它接受的是慷慨的。 用户可以使用int或Future <int>调用该方法,因此无论如何他们都不需要在Future中包装一个int。
如果返回FutureOr <int>,用户需要检查是否可以返回int或Future <int>,然后才能执行任何有用的操作。 (或者他们只是等待价值,实际上总是将其视为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 as Future<int>).then((v) => v * 3);
}
这条建议指南更精确的表述是仅在逆变位置使用FutureOr <T>。 参数是逆变的,返回类型是协变的。 在嵌套函数类型中,这会被翻转 - 如果你有一个类型本身就是函数的参数,那么回调的返回类型现在处于逆变位置,并且回调的参数是协变的。 这意味着回调的类型可以返回FutureOr <T>:
Stream<S> asyncMap<T, S>(
Iterable<T> iterable, FutureOr<S> Function(T) callback) async* {
for (var element in iterable) {
yield await callback(element);
}
}
4.7 参数
4.7.1 避免布尔类型的可选参数
与其他类型不同,布尔值通常以字面形式使用。 数字之类的东西通常包含在命名常量中,但我们通常只是直接传递true和false。 如果不清楚boolean表示的是什么,这可能使调用点不可读:
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;
4.7.2 如果用户可能想要省略先前的参数,则避免可选的位置参数
可选的位置参数应该具有逻辑进展,使得较早的参数比后面的参数更频繁地传递。 用户几乎不需要显式传递“漏洞”来省略先前的位置参数以便稍后传递。 你最好使用命名参数。
String.fromCharCodes(Iterable 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});
4.7.3 避免接受特殊“无参数”值的强制参数
如果用户在逻辑上省略了参数,则实际上应该省略它,办法是使参数可选而不是强制它们传递null,空字符串或其他一些意味着“没有通过”的特殊值。
省略参数更简洁,有助于防止在用户认为提供实际值时偶然传递像null这样的标记值的错误。
比如:
var rest = string.substring(start);
而不是:
var rest = string.substring(start, null);
4.7.4 请使用包含开始和独占结束参数来接受有范围的参数
如果要定义允许用户从某个整数索引序列中选择一系列元素或项的方法或函数,请使用一个起始索引,该索引引用第一个项和一个(可能是可选的)结束索引,该索引比一个大于 最后一项的索引。
这与执行相同操作的核心库一致。
比如:
[0, 1, 2, 3].sublist(1, 3) // [1, 2]
‘abcd’.substring(1, 3) // ‘bc’
在这里保持一致尤为重要,因为这些参数通常是未命名的。 如果您的API需要一个长度而不是一个终点,那么在调用点上根本不会显示差异。
4.8 相等
为类实现自定义相等行为可能很棘手。 用户对于对象需要匹配的相等如何工作有着深刻的直觉,而哈希表之类的集合类型具有他们期望元素遵循的微妙约定。
4.8.1 如果复写==,请复写hashCode
4.8.2 确保你的==运算符遵守平等的数学规则
4.8.3 避免定义可变类的自定义相等
4.8.4 不要在传统的 ==运算符中检查null
Dart指定此检查是自动完成的,只有在右侧不为空时才调用==方法:
比如:
class Person {
final String name;
// ···
bool operator ==(other) => other is Person && name == other.name;
int get hashCode => name.hashCode;
}
而不是:
class Person {
final String name;
// ···
bool operator ==(other) => other != null && …
}