[TOC]
在程序中记下类型时,会限制流入代码不同部分的值的类型。类型可以出现在两种地方:声明上的类型注释和泛型调用的类型参数。
当您想到“静态类型”时,类型注释是您通常会想到的。您可以键入注释变量,参数,字段或返回类型。在以下示例中,bool和String为类型注释。它们挂起代码的静态声明结构,并且不会在运行时“执行”。
~~~
bool isEmpty(String parameter) {
bool result = parameter.length == 0;
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同时具有类型推断和动态类型的事实导致了人们对代码是“非类型”的含义的困惑。这是否意味着代码是动态类型的,或者您没有编写类型?为了避免混淆,我们避免说“untyping”,而是使用以下术语:
* 如果代码是带类型注释的,则该类型显式地写在代码中。
* 如果推断出代码,则没有编写类型注释,Dart自己成功地找到了类型。推理可能会失败,在这种情况下,指南不考虑推断。在某些地方,推理失败是静态错误。在其他情况下,Dart使用了动态备份类型。
* 如果代码是动态的,那么它的静态类型就是特殊的动态类型。代码可以被显式地注释为动态的,也可以被推断出来。
换句话说,某些代码是注释的还是推断的,与它是动态的还是其他类型的正交。
推理是一种强大的工具,可以让您省去编写和阅读那些明显或无趣的类型的工作。在明显的情况下省略类型也会将读者的注意力吸引到显式类型上,因为这些类型很重要,比如强制类型转换。
显式类型也是健壮,可维护代码的关键部分。它们定义了API的静态形状。它们记录并强制允许哪些值允许到达程序的不同部分。
这里的指导方针在我们在简洁性和明确性,灵活性和安全性之间找到了最佳平衡。在决定要编写哪些类型时,您需要回答两个问题:
* 我应该写哪种类型,因为我认为最好让它们在代码中可见?
* 我应该写哪种类型因为推理无法为我提供这些类型?
这些指南可以帮助您回答第一个问题:
* 如果类型不明显,则优先对公共字段和顶级变量进行类型注释。
* 如果类型不明显,请考虑对私有字段和顶级变量进行类型注释。
* 避免类型注释初始化的局部变量。
* 避免在函数表达式上注释推断的参数类型。
* 避免泛型调用上的冗余类型参数。
这些涵盖了第二个:
* 当Dart推断出错误的类型时,请进行注释。
* 优先使用动态注释,而不是让推断失败。
其余指南涵盖了有关类型的其他更具体的问题。
## 如果类型不明显,则优先对公共字段和顶级变量进行类型注释。
类型注释是关于如何使用库的重要文档。它们在程序的区域之间形成边界以隔离类型错误的来源。考虑:
~~~
【bad】
install(id, destination) => ...
~~~
这里,id是什么还不清楚。一个字符串?目的是什么?字符串还是文件对象?这个方法是同步的还是异步的?这是清晰的:
~~~
Future<bool> install(PackageId id, String destination) => ...
~~~
但在某些情况下,类型是如此明显,以至于编写它是毫无意义的:
~~~
const screenWidth = 640; // Inferred as int.
~~~
“显而易见”并没有明确的定义,但这些都是很好的候选者:
* 字面量。
* 构造函数调用。
* 对显式类型化的其他常量的引用。
* 数字和字符串的简单表达式。
* 工厂方法,如int.parse()、Future.wait()等,读者应该很熟悉。
如果有疑问,请添加类型注释。即使类型很明显,您可能仍然希望显式注释。如果推断类型依赖于来自其他库的值或声明,您可能希望键入注释您的声明,以便对其他库的更改不会在您没有意识到的情况下悄无声息地更改您自己的API的类型。
## 如果类型不明显,请考虑对私有字段和顶级变量进行类型注释。
在公开声明上键入注释可以帮助代码的用户。私有成员上的类型帮助维护人员。私有声明的范围更小,那些需要知道声明类型的人也更可能熟悉周围的代码。这使得更依赖于推理和省略私有声明类型变得合理,这就是为什么这个指南比上一个指南更温和的原因。
如果您认为初始化器表达式(无论它是什么)足够清晰,那么您可以省略注释。但是如果您认为注释有助于使代码更清晰,那么添加一个注释。
## 避免类型注释初始化的局部变量。
局部变量的作用域非常小,尤其是在函数往往很小的现代代码中。省略类型会将读者的注意力集中在变量的更重要的名称及其初始值上。
~~~
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;
}
~~~
~~~
【bad】
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;
}
~~~
## 避免在函数表达式上注释推断的参数类型。
匿名函数几乎总是立即传递给具有某种回调的方法。(如果没有立即使用该函数,通常值得将其命名为声明。)当在类型化上下文中创建函数表达式时,Dart试图根据预期的类型推断函数的参数类型。
例如,当您将函数表达式传递给Iterable.map()时,根据map()期望的回调类型推断函数的参数类型:
~~~
var names = people.map((person) => person.name);
~~~
~~~
【bad】
var names = people.map((Person person) => person.name);
~~~
在极少数情况下,周围的上下文不够精确,无法为一个或多个函数参数提供类型。在这些情况下,您可能需要注释。
## 避免泛型调用上的冗余类型参数。
如果推理要填充相同的类型,那么类型参数就是冗余的。如果调用是类型注释变量的初始化器,或者是函数的参数,那么推断通常会为您填充类型:
~~~
Set<String> things = Set();
~~~
~~~
【bad】
Set<String> things = Set<String>();
~~~
这里,变量上的类型注释用于推断初始化器中构造函数调用的类型参数。
在其他情况下,没有足够的信息来推断类型,然后你应该写类型参数:
~~~
var things = Set<String>();
~~~
~~~
【bad】
var things = Set();
~~~
在这里,由于变量没有类型注释,因此没有足够的上下文来确定要创建哪种类型的集合,因此应该显式地提供类型参数。
## 当Dart推断出错误的类型时,请进行注释。
有时,Dart推断出一种类型,但不是你想要的类型。例如,您可能希望变量的类型是初始化程序类型的超类型,以便稍后可以为变量分配一些其他同级类型:
~~~
num highScore(List<num> scores) {
num highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
~~~
~~~
【bad】
num highScore(List<num> scores) {
var highest = 0;
for (var score in scores) {
if (score > highest) highest = score;
}
return highest;
}
~~~
在这里,如果分数包含double值,比如[1.2],那么赋值最高的值就会失败,因为它的推断类型是int,而不是num。
## 优先使用动态注释,而不是让推断失败。。
Dart允许您在许多地方省略类型注释,并尝试为您推断类型。在某些情况下,如果推理失败,它将无声地为您提供动态。如果您想要的是dynamic,那么从技术上来说,这是最简洁的方法。
然而,这并不是最明确的方式。如果您的代码的临时读者看到注释不见了,那么他就无法知道您是否希望它是动态的、预期的填充其他类型的推断,或者只是忘记编写注释。
当您想要的类型是dynamic时,明确地编写它可以使您的意图变得清晰。
~~~
dynamic mergeJson(dynamic original, dynamic changes) => ...
~~~
~~~
【bad】
mergeJson(original, changes) => ...
~~~
>在Dart 2之前,这条指南提出了完全相反的观点:当它是隐含的时,不要用动态注释。有了新的更强大的类型系统和类型推断,用户现在希望Dart表现得像一种推断的静态类型语言。有了这个心智模型,发现一个代码区域已经悄无声息地失去了所有静态类型的安全性和性能是一个令人不快的意外。
>
## 优先选择函数类型注释中签名。。
标识符函数本身没有任何返回类型或参数签名,是指特殊的函数类型。这种类型只比使用dynamic稍微有用一点。如果要注释,请选择包含参数和函数返回类型的完整函数类型。
~~~
bool isValid(String value, bool Function(String) test) => ...
~~~
~~~
【bad】
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.");
}
}
}
~~~
## 不要为setter指定返回类型。
在Dart中setter总是返回void。所以设定void类型毫无意义。
~~~
【bad】
void set foo(Foo value) { ... }
~~~
~~~
set foo(Foo value) { ... }
~~~
## 不要使用遗留类型定义语法。
Dart有两种符号,用于为函数类型定义命名的typedef。原始语法如下:
~~~
【bad】
typedef int Comparison<T>(T a, T b);
~~~
该语法有几个问题:
* 没有办法将名称分配给泛型函数类型。在上面的例子中,typedef本身是通用的。如果在代码中引用比较,没有类型参数,就会隐式地得到函数类型int函数(dynamic, dynamic),而不是int Function\<T>(T, T)。
* 参数中的单个标识符被解释为参数的名称,而不是其类型。考虑到:
~~~
【bad】
typedef bool TestNumber(num);
~~~
大多数用户期望这是一个取num并返回bool的函数类型。它实际上是一个接受任何对象(动态)并返回bool的函数类型。该参数的名称(除了typedef文档外,它不用于任何内容)是“num”。这是Dart长期以来的错误来源。
新语法如下所示:
~~~
typedef Comparison<T> = int Function(T, T);
~~~
如果要包含参数的名称,也可以这样做:
~~~
typedef Comparison<T> = int Function(T a, T b);
~~~
新语法可以表达旧语法可以表达的任何内容,而且缺少容易出错的错误特性,即将单个标识符视为参数的名称而不是其类型。在typedef中=后面的相同函数类型语法也被允许出现类型注释的任何地方,这给了我们一种在程序中任何地方编写函数类型的统一方法。
旧的typedef语法仍然被支持以避免破坏现有的代码,但它是不赞成的。
## 优先使用内联函数类型而不是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仍然是值得的。但在大多数情况下,用户希望看到函数类型在使用它的地方是正确的,而函数类型语法使他们更清楚。
## 考虑对参数使用函数类型语法。
当定义类型为函数的参数时,Dart具有特殊的语法。有点像在C,你包围参数的名字与函数的返回类型和参数签名:
~~~
Iterable<T> where(bool predicate(T element)) => ...
~~~
在Dart 2添加函数类型语法之前,这是在没有定义类型定义的情况下为参数提供函数类型的唯一方法。既然Dart对函数类型有了一个通用符号,那么您也可以将其用于函数类型参数:
~~~
Iterable<T> where(bool Function(T) predicate) => ...
~~~
新的语法有点冗长,但与必须使用新语法的其他位置一致。
## 使用Object而不是dynamic进行注释,以指示允许任何对象。
有些操作适用于任何可能的对象。例如,log()方法可以接受任何对象并在其上调用toString()。Dart中有两种类型允许所有的值:对象和动态。然而,它们传达的信息不同。如果您只想声明您允许所有对象,那么就使用Object,就像在Java或c#中那样。
使用dynamic发送更复杂的信号。这可能意味着Dart的类型系统不够复杂,无法表示允许的类型集,或者值来自互操作或静态类型系统范围之外,或者您明确希望在程序中的某个地方使用运行时动态。
~~~
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.');
}
~~~
## 使用Future作为不产生值的异步成员的返回类型。
当您有一个不返回值的同步函数时,您使用void作为返回类型。异步等效项是Future\<void>。
您可能会看到使用Future或Future\<Null>的代码,因为Dart的旧版本不允许void作为类型参数。既然这样,你应该使用它。这样做更直接地匹配您键入类似的同步函数的方式,并为调用者和函数体提供更好的错误检查。
## 避免使用FutureOr\<T>作为返回类型。
如果一个方法接受FutureOr\<int>,那么它接受的内容是宽松的。用户可以使用int或Future\<int>来调用方法,因此在以后您要解包的int中,他们不需要包装它。
如果您返回一个FutureOr\<int>,用户需要在做任何有用的事情之前检查是否返回一个int或一个Future\<int>。(或者他们只是在等待价值的Future,实际上他们总是把它当作Future。)返回一个Future\<int>,这个很明确。用户更容易理解一个函数要么总是异步的,要么总是同步的,但是一个函数要么总是异步的,很难正确使用。
~~~
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
~~~
~~~
【bad】
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);
}
}
~~~