如何阅读指南
-
DO
应始终遵循的准则 -
DON'T
不应该这么使用的准则 -
PREFER
应该遵循的准则,但是在某些情况下,可以根据个人理解忽略,不遵循 -
AVOID
不应该遵循的准则,但是在某些情况下,可以根据个人理解忽略,不遵循 -
CONSIDER
可遵循或者不遵循的规则,取决于个人偏好
库
这些指南可帮助您以一致、可维护的方式从多个文件中编写程序。为了简化这些指导原则,他们使用“import”来涵盖import
和export
指示.该指南同样适用于两者。
DO part of指令中使用字符串
如果您确实选择使用 part将部分库拆分为另一个文件,则Dart要求另一个文件依次指示它所属的库。
library my_library;
part "some/other/file.dart";
// Good
part of "../../my_library.dart";
// Bad
part of my_library;
DON’T 导入另一个包的src目录中的库
lib下的src目录被指定为由自己实现的私有库。包维护人员可以自由地对src下的代码进行彻底的更改,而不会对包造成破坏。
这意味着,如果您导入其他包的私有库,那么该软件包的一个次要的、理论上非破坏性的版本可能都会破坏你的代码。
PREFER 导入包的lib目录中的库时的使用相对路径
Linter规则:avoid_relative_lib_imports
当lib从同一个包中的另一个库引用包的目录中的库时相对URI
或显式package:
都能实现。
例如,假设你的目录结构如下:
my_package
└─ lib
├─ src
│ └─ utils.dart
└─ api.dart
如果api.dart想要导入utils.dart,它应该使用:
// Good
import 'src/utils.dart';
// Bad
import 'package:my_package/src/utils.dart';
遵循以下两条规则:
- 导入路径永远不应包含
/lib/
- lib下的库不应该使用
../
要转义lib
目录。
字符串
DO 使用相邻的字符串来连接字符串文字
Linter规则:prefer_adjacent_string_concatenation
如果你有两个字符串文字——不是值,而是实际引用的文字形式——你不需要+
用来连接它们。就像在C和C ++中一样,只需将它们放在一起就可以了
。这是制作不适合一行的单个长字符串的好方法。
// Good
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.');
// Bad
raiseAlarm('ERROR: Parts of the spaceship are on fire. Other ' +
'parts are overrun by martians. Unclear which are which.');
PREFER 使用插值来组合字符串和值
Linter规则:prefer_interpolation_to_compose_strings
// Good
'Hello, $name! You are ${year - birth} years old.';
// Bad
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
AVOID 在不需要时使用花括号进行插值
Linter规则:unnecessary_brace_in_string_interps
// Good
'Hi, $name!'
"Wear your wildest $decade's outfit."
'Wear your wildest ${decade}s outfit.'
// Bad
'Hi, ${name}!'
"Wear your wildest ${decade}'s outfit."
集合
Dart支持四种集合类型:lists(列表), maps(映射), queues(队列),sets(集合)。
DO 尽可能使用集合字面意义的方式
有两种方法可以制作一个空的可增长列表:[]
和List()
。同样,有三种方法可以使一个空的哈希映射:{}, Map(),和LinkedHashMap()
。
如果要创建不可扩展的列表或其他一些自定义集合类型
,那么请务必使用构造函数
。否则,请使用字面意义
的语法。核心库公开了那些构造函数以便于采用,但惯用的Dart代码不使用它们。
// Good
var points = [];
var addresses = {};
// Bad
var points = List();
var addresses = Map();
如果重要的话,你甚至可以为它们提供一个类型参数。
// Good
var points = <Point>[];
var addresses = <String, Address>{};
// Bad
var points = List<Point>();
var addresses = Map<String, Address>();
注意,这并不适用于这些类的命名构造函数
。List.from()、Map.fromIterable()都有它们的用途。如果你传递一个大小来通过List()创建一个不可增长的大小,那么使用构造函数的方式创建是有意义的。
DON’T 用.length来查看集合是否为空
调用.length
只是为了查看集合是否包含任何内容可能会非常缓慢。相反,有更快,更可读的getter:.isEmpty
和 .isNotEmpty
// Good
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
// Bad
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');
CONSIDER 使用高阶方法转换序列
如果你有一个集合并希望从它产生一个新修改的集合
,使用.map(),.where()以及其他方便的方法
,往往更短,更简明。使用这些而不是命令性for
循环可以清楚地表明您的意图是生成新序列而不产生副作用。
var aquaticNames = animals
.where((animal) => animal.isAquatic)
.map((animal) => animal.name);
AVOID 使用Iterable.forEach()函数
Linter规则:avoid_function_literals_in_foreach_calls
forEach()函数在JavaScript中被广泛使用,因为内置 for-in循环不能达到您通常想要的效果。在Dart中,如果要迭代序列,那么惯用的方法就是使用循环。
// Good
for (var person in people) {
...
}
// Bad
people.forEach((person) {
...
});
例外情况是,如果要执行的操作是调用一些已存在的函数,并将每个元素作为参数。在那种情况下,forEach()很方便。
people.forEach(print);
DON’T 使用List.from(),除非您打算更改结果的类型
给定Iterable,有两种显而易见的方法可以生成包含相同元素的新List:
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
最明显的区别是第一个更短。重要的区别是,第一个保留了原始对象的类型参数:
// Good
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<int>":
print(iterable.toList().runtimeType);
// Bad
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);
如果要更改类型,则调用List.from()很有用:
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);
但是,如果您的目标只是复制iterable并保留其原始类型,或者您不关心类型,那么请使用toList()。
DO whereType()按类型过滤集合
Linter规则:prefer_iterable_whereType
假设您有一个包含对象混合的数组,并且您希望只获取整数。你可以where()像这样使用:
// Good
var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType<int>();
// Bad
// 它返回一个可能不是你想要的类型的iterable。在这里的示例中,它返回一个Iterable<Object>即使你可能想要一个,Iterable<int>因为那是你要过滤它的类型。
var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int);
// 这是冗长的,并导致创建两个包装器,具有两层间接和冗余运行时检查。
var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int).cast<int>();
DON’T 当附近的操作可以执行时,使用cast()
// Good
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
// Bad
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();
// Good
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
// Bad
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();
AVOID 使用cast()
这是对上一个规则的更为柔和的概括。有时候没有附近的操作可以用来修复某些对象的类型。即便如此,尽可能避免使用cast()更改集合的类型。
请改为选择以下任何选项:
使用正确的类型创建它。更改首次创建集合的代码时,使其具有正确的类型。
在访问时强制转换元素。如果您立即迭代集合,则在迭代内部转换每个元素。
使用List.from()。如果您最终将访问集合中的大多数元素,并且您不需要该对象由原始活动对象支持,请使用它进行转换List.from()。
该cast()
方法返回一个惰性
集合,该集合检查每个操作的元素类型。如果只对少数
元素执行少量操作,那么懒惰就会很好。但在许多情况下,延迟验证和包装的开销超过了好处。
以下是使用正确类型创建它的示例:
// Good
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
// Bad
List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}
这是在访问时转换每个元素:
// Good
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);
}
}
// Bad
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()
// Good
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];
}
// Bad
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()是正确的答案。但是考虑到这种方法有点危险和不可取——它可能很慢,如果你不小心,可能会在运行时失败。
功能
DO 使用函数声明将函数绑定到名称
Linter规则:prefer_function_declarations_over_variables
// Good
void main() {
localFunction() {
...
}
}
// Bad
void main() {
var localFunction = () {
...
};
}
DON’T 当省略时创建lambda
Linter规则:unnecessary_lambdas
如果您引用了对象上的一个方法,但是省略了圆括号,那么Dart会给您一个“tearoff”——一个闭包,它接受与该方法相同的参数,并在您调用它时调用它。
如果有一个函数调用的方法具有与传递给它的参数相同的参数,则不需要手动将调用包装在lambda中。
// Good
names.forEach(print);
// Bad
names.forEach((name) {
print(name);
});
参数
DO 使用 = 将命名参数与其默认值分隔开
Linter规则:prefer_equal_for_default_values
// Good
void insert(Object item, {int at = 0}) { ... }
// Bad
void insert(Object item, {int at: 0}) { ... }
DON’T 显式的设置默认值为null
Linter规则:avoid_init_to_null
// Good
void error([String message]) {
stderr.write(message ?? '\n');
}
// Bad
void error([String message = null]) {
stderr.write(message ?? '\n');
}
变量
DON’T 将变量显式初始化为null
在Dart中,未自动显式初始化的变量或字段将初始化为null。这是由语言可靠地指定的。Dart中没有“未初始化记忆”的概念。添加 = null 是多余的,不需要。
// Good
int _nextId;
class LazyId {
int _id;
int get id {
if (_nextId == null) _nextId = 0;
if (_id == null) _id = _nextId++;
return _id;
}
}
// Bad
int _nextId = null;
class LazyId {
int _id = null;
int get id {
if (_nextId == null) _nextId = 0;
if (_id == null) _id = _nextId++;
return _id;
}
}
AVOID 存储可以计算的内容
在设计类时,经常希望将多个视图暴露给相同的基础状态。通常,您会看到在构造函数中计算所有这些视图的代码,然后存储它们:
// Bad
class Circle {
num radius;
num area;
num circumference;
Circle(num radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
这段代码有两个问题。首先,它可能会浪费内存。严格地说,面积和周长是缓存。它们是存储的计算,我们可以从已有的其他数据中重新计算。他们正在用增加的内存换取减少的CPU使用。
更糟糕的是,代码是错误的。缓存的问题是无效——您如何知道缓存何时过期并需要重新计算?在这里,我们从不这样做,即使半径是可变的。您可以指定一个不同的值,那么面积和周长将保留它们以前的、现在不正确的值。
为了正确处理缓存失效,我们需要这样做:
// Bad
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;
}
成员
在Dart中,对象具有可以是函数(方法)或数据(实例变量)的成员
DON’T 在getter和setter中不必要地包装字段
Linter规则:unnecessary_getters_setters
在Java和C#中,通常隐藏getter和setter(或C#中的属性)后面的所有字段,即使实现只是转发到字段。这样,如果你需要在这些成员中做更多的工作,你可以不需要触摸呼叫。这是因为调用getter方法与访问Java中的字段不同,访问属性与访问C#中的原始字段不是二进制兼容的。
Dart没有这个限制。字段和getter / setter是完全无法区分的。您可以在类中公开一个字段,然后将其包装在getter和setter中,而不必触及任何使用该字段的代码。
// Good
class Box {
var contents;
}
// Bad
class Box {
var _contents;
get contents => _contents;
set contents(value) {
_contents = value;
}
}
PREFER 使用final字段来创建只读属性
如果您有一个外部代码应该能够看到但不能分配给它的字段,那么在许多情况下,一个简单的解决方案就是将其标记为final。
// Good
class Box {
final contents = [];
}
// Bad
class Box {
var _contents;
get contents => _contents;
}
当然,如果您需要在构造函数外部对字段进行内部分配,您可能需要执行“private field, public getter”模式,但在需要之前不要进行此操作。
CONSIDER 使用=>简单的成员
Linter规则:prefer_expression_function_bodies
除了=>用于函数表达式之外,Dart还允许您使用它来定义成员。该样式非常适合仅计算和返回值的简单成员。
double get area => (right - left) * (bottom - top);
bool isReady(num time) => minTime == null || minTime <= time;
String capitalize(String name) =>
'${name[0].toUpperCase()}${name.substring(1)}';
人们编写的代码似乎更喜欢=>,但它很容易滥用它,并用代码很难结束阅读。如果您的声明超过几行或包含深层嵌套的表达式——级联和条件运算符是常见的违规者, 请使用块体和一些语句。
// Good
Treasure openChest(Chest chest, Point where) {
if (_opened.containsKey(chest)) return null;
var treasure = Treasure(where);
treasure.addAll(chest.contents);
_opened[chest] = treasure;
return treasure;
}
// Bad
Treasure openChest(Chest chest, Point where) =>
_opened.containsKey(chest) ? null : _opened[chest] = Treasure(where)
..addAll(chest.contents);
您还可以对不返回值的成员使用=>。当setter很小并且有一个使用=>的相应getter时,这是一种习惯用法。
num get x => center.x;
set x(num value) => center = Point(value, center.y);
DON’T 用 this. 当不需要避免shadowing时
Linter规则:unnecessary_this
JavaScript要求显式this.引用当前正在执行其方法的对象上的成员,但类似Dart的C ++,Java和C#不具有该限制。
您唯一需要使用的this.是当具有相同名称的局部变量隐藏要访问的成员时。
// Bad
class Box {
var value;
void clear() {
this.update(null);
}
void update(value) {
this.value = value;
}
}
// Good
class Box {
var value;
void clear() {
update(null);
}
void update(value) {
this.value = value;
}
}
请注意,构造函数参数从不影响构造函数初始化列表中的字段:
class Box extends BaseBox {
var value;
Box(value)
: value = value,
super(value);
}
DO 尽可能在声明时初始化字段
如果字段不依赖于任何构造函数参数,那么应该在声明时初始化它。它使用更少的代码,并且确保如果类有多个构造函数,您不会忘记初始化它。
// Good
class Folder {
final String name;
final List<Document> contents = [];
Folder(this.name);
Folder.temp() : name = 'temporary';
}
// Bad
class Folder {
final String name;
final List<Document> contents;
Folder(this.name) : contents = [];
Folder.temp() : name = 'temporary'; // Oops! Forgot contents.
}
当然,如果字段依赖于构造函数参数,或者由不同的构造函数进行不同的初始化,则本指南不适用
构造函数
DO 尽可能使用初始化形式
Linter规则:prefer_initializing_formals
// Good
class Point {
num x, y;
Point(this.x, this.y);
}
// Bad
class Point {
num x, y;
Point(num x, num y) {
this.x = x;
this.y = y;
}
}
this.
构造函数参数之前的语法称为“初始化形式”。你不能总是利用它。特别是,使用它意味着参数在初始化列表中不可见。但是,当你能做到的时候,你就应该去做。
DON’T 类型注释初始化形式
Linter规则:type_init_formals
// Good
class Point {
int x, y;
Point(this.x, this.y);
}
// Bad
class Point {
int x, y;
Point(int this.x, int this.y);
}
DO 使用;而不是{}空构造函数体
Linter规则:empty_constructor_bodies
在Dart中,具有空主体的构造函数可以用分号结束。(实际上,const构造函数需要它)。
// Good
class Point {
int x, y;
Point(this.x, this.y);
}
// Bad
class Point {
int x, y;
Point(this.x, this.y) {}
}
DON’T 使用new
Dart 2使new关键字成为可选的。即使在Dart 1中,它的含义也从来没有明确过,因为工厂构造函数意味着新的调用可能仍然不会实际返回新的对象。
为了减少迁移的痛苦,这种语言仍然允许使用new,但是考虑到它已被弃用并从代码中删除它。
// Good
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(
child: Text('Increment'),
),
Text('Click!'),
],
);
}
// Bad
Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(
child: new Text('Increment'),
),
new Text('Click!'),
],
);
}
DON’T 多余地使用const
Linter规则:unnecessary_const
在表达式必须是常量的上下文中,const关键字是隐式的,不需要写,也不应该写。这些上下文是其中的任何表达式:
- const集合文字。
- const构造函数调用
- 元数据注释
- const变量声明的初始化
- 交换用例表达式 - 紧接在case之前的部分:,而不是用例的主体。
默认值不包括在这个列表中,因为Dart的未来版本可能支持非const默认值)
基本上,在写new而不是const时会出错的任何地方,Dart 2都允许您省略const
// Good
const primaryColors = [
Color("red", [255, 0, 0]),
Color("green", [0, 255, 0]),
Color("blue", [0, 0, 255]),
];
// Bad
const primaryColors = const [
const Color("red", const [255, 0, 0]),
const Color("green", const [0, 255, 0]),
const Color("blue", const [0, 0, 255]),
];
错误处理
AVOID 捕捉没有 on 的错误子句
Linter规则:avoid_catches_without_on_clauses
一个没有on限定符的catch子句捕获try块中的代码抛出的任何内容。Pokemon异常处理很可能不是您想要的。您的代码是否正确地处理了StackOverflowError或OutOfMemoryError?如果您错误地将错误的参数传递给try块中的方法,您是希望调试器将错误指向您,还是希望有用的ArgumentError被吞噬?在捕获抛出的assertionerror之后,您希望代码中的任何assert()语句有效地消失吗?
答案可能是“否”,在这种情况下,您应该过滤捕获的类型。在大多数情况下,您应该有一个on子句,它将您限制到您所知道的和正在正确处理的运行时故障的类型。
在极少数情况下,您可能希望捕获任何运行时错误。这通常是在框架或底层代码中,这些代码试图将任意应用程序代码与问题隔离开来。即使在这里,捕获异常通常也比捕获所有类型要好。Exception是所有运行时错误的基类,并排除指示代码中编程错误的错误。
DON’T 没有on子句情况下,放弃捕获的错误,
如果您确实觉得需要捕获代码区域中可以抛出的所有内容,那么就用捕获的内容做一些事情。记录它,将它显示给用户或重新抛出它,但不要悄悄地丢弃它。
DO 抛出Error仅用于编程错误的对象
Error类是程序错误的基类。当抛出该类型的对象或它的一个子接口(如ArgumentError)时,意味着代码中存在错误。当您的API想要向调用者报告它正在被错误地使用时,抛出一个错误会清楚地发送该信号。
相反,如果异常是某种运行时故障,而该故障并不表示代码中有错误,那么抛出错误是一种误导。相反,抛出一个核心异常类或其他类型。
DON’T 显式捕获错误或实现错误的类型
这是由上面得出的。由于错误指示代码中的错误,因此它应该展开整个调用堆栈,停止程序,并打印堆栈跟踪信息,以便找到并修复错误。
捕捉这些类型的错误会破坏该过程并掩盖错误。不要在异常发生后添加错误处理代码来处理该异常,而是返回并修复最初导致异常抛出的代码。
DO 使用rethrow重新抛出捕获的异常
Linter规则:use_rethrow_when_possible
如果决定重新抛出异常,请使用rethrow语句,而不是使用throw抛出相同的异常对象。rethrow保留异常的原始堆栈跟踪。另一方面,throw将堆栈跟踪重置为最后一个抛出的位置。
// Good
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
// Bad
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
异步
PREFER 使用async/await
众所周知,异步代码很难阅读和调试,即使使用像futures这样的抽象也是如此。async/ wait语法提高了可读性,允许您在异步代码中使用所有Dart控制流结构。
// Good
Future<int> countActivePlayers(String teamName) async {
try {
var team = await downloadTeam(teamName);
if (team == null) return 0;
var players = await team.roster;
return players.where((player) => player.isActive).length;
} catch (e) {
log.error(e);
return 0;
}
}
// Bad
Future<int> countActivePlayers(String teamName) {
return downloadTeam(teamName).then((team) {
if (team == null) return Future.value(0);
return team.roster.then((players) {
return players.where((player) => player.isActive).length;
});
}).catchError((e) {
log.error(e);
return 0;
});
}
DON’T 在没有用时使用async
在任何执行与异步相关的操作的函数上都很容易养成使用异步的习惯。但在某些情况下,这是无关紧要的。如果可以在不更改函数行为的情况下可以省略异步,那么就省略。
// Good
Future afterTwoThings(Future first, Future second) {
return Future.wait([first, second]);
}
// Bad
Future afterTwoThings(Future first, Future second) async {
return Future.wait([first, second]);
}
以下情况 async 是有用的:
正在使用await。(这是显而易见的。)
正在异步返回一个错误。async比throw跑出更短的return Future.error(...)。
正在返回一个值,并且希望在将来隐式地包装它。 async比Future.value(...)短
Future usesAwait(Future later) async {
print(await later);
}
Future asyncError() async {
throw 'Error!';
}
Future asyncValue() async => 'value';
CONSIDER 使用高阶方法转换流
这与上述关于迭代的建议相似。Streams支持许多相同的方法,并且还可以正确处理传输错误、关闭等操作。
AVOID 直接使用Completer
许多刚接触异步编程的人想要编写可以产生未来的代码。Future中的构造函数似乎不符合他们的需要,因此他们最终找到了Completer类并使用它
// Bad
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}
两种低级代码需要Completer: new asynchronous primitives以及与不使用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');
}
DO 在消除类型参数为Object的FutureOr<T>的歧义时,测试Future<T>。
可以用做任何有用的FutureOr<T>事情之前,通常需要用is检查是否有一个Future<T>或一个空的T。如果类型参数是某些特定类型的FutureOr<int>,在不重要的测试中可以使用is int或is Future<int>。两者都有效,因为这两种类型是不相交的。
但是,如果值类型是Object或可能用Object实例化的类型参数,则这两个分支重叠。Future<Object>本身实现Object,所以is Object或is T,其中T是可以用Object实例化的类型参数,即使对象是Future,也返回true。相反,显式地测试未来的情况:
// Good
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;
}
}
// Bad
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;
}
}
在这个糟糕的示例中,如果您传递给它一个Future<Object>,它会错误地将其视为一个空的同步值。