术语表
以下是 Dart 文档中使用的术语定义。
辅助操作
一种自动化的本地代码编辑,旨在对代码进行常见改进。
辅助操作是一种自动化的本地代码编辑,旨在对代码进行常见改进。辅助操作的例子包括将 switch
语句转换为 switch
表达式,反转 if
语句中的 then
和 else
块,以及将 widget 插入到 widget 结构中。
相关文档与资源
常量上下文
代码中 const
关键字隐式存在,并且该区域内的一切都必须是常量的区域。
常量上下文是代码中的一个区域,在该区域内无需包含 const
关键字,因为该区域中的所有内容都必须是常量,const
关键字是隐式存在的。以下位置是常量上下文:
以
const
关键字为前缀的列表、映射或集合字面量中的所有内容。例如:dartvar l = const [/*constant context*/];
常量构造函数调用中的参数。例如:
dartvar p = const Point(/*constant context*/);
以
const
关键字为前缀的变量的初始化表达式。例如:dartconst v = /*constant context*/;
注解。
case
子句中的表达式。例如:dartvoid f(int e) { switch (e) { case /*constant context*/: break; } }
确定性赋值
判断一个变量在使用前是否已被确定性地赋值。
确定性赋值分析是针对代码中每个点的每个局部变量,判断以下哪个条件成立的过程:
- 变量已被确定性地赋值(确定性已赋值)。
- 变量已被确定性地未赋值(确定性未赋值)。
- 变量可能已被赋值,也可能未被赋值,取决于到达该点的执行路径。
确定性赋值分析有助于发现代码中的问题,例如引用可能未被赋值的变量的位置,或者在变量可能已被赋值之后再次对其进行赋值(该变量只能赋值一次)的位置。
例如,在以下代码中,当变量 s
作为参数传递给 print
时,它是确定性未赋值的:
void f() {
String s;
print(s);
}
但在以下代码中,变量 s
是确定性已赋值的:
void f(String name) {
String s = 'Hello $name!';
print(s);
}
确定性赋值分析甚至可以在存在多条可能的执行路径时判断变量是否确定性已赋值(或未赋值)。在以下代码中,如果执行通过 if
语句的 true 分支或 false 分支,都会调用 print
函数,但由于无论选择哪个分支 s
都会被赋值,因此在将其传递给 print
之前,它已被确定性地赋值。
void f(String name, bool casual) {
String s;
if (casual) {
s = 'Hi $name!';
} else {
s = 'Hello $name!';
}
print(s);
}
在控制流分析中,if
语句的末尾被称为一个汇合点(join)——即两条或多条执行路径重新合并的地方。在汇合点,如果一个变量在所有合并的路径上都确定性已赋值,那么该变量就是确定性已赋值的;如果它在所有路径上都确定性未赋值,那么它就是确定性未赋值的。
有时一个变量在一条路径上被赋值,但在另一条路径上没有,这种情况下该变量可能已被赋值,也可能未被赋值。在以下示例中,if
语句的 true 分支可能执行,也可能不执行,因此变量可能已被赋值,也可能未被赋值:
void f(String name, bool casual) {
String s;
if (casual) {
s = 'Hi $name!';
}
print(s);
}
如果存在一个不给 s
赋值的 false 分支,情况也一样。
循环的分析稍微复杂一些,但遵循同样的基本原理。例如,while
循环中的条件总是执行的,但循环体可能执行,也可能不执行。因此,就像 if
语句一样,在 while
语句的末尾,条件为 true
的路径与条件为 false
的路径之间存在一个汇合点。
相关文档与资源
不可反驳模式
总是匹配的模式。
Late
一个关键字,用于延迟初始化变量,通常用于非空变量。
Dart 中的 late
关键字用于指示一个变量将在其声明之后、使用之前被初始化。当你确定变量最终会获得一个值,只是不是立即获得时,这有助于避免将其声明为可空 (?
)。
使用 late
会延迟初始化,使你能够编写更灵活、更易读的代码,尤其是在处理依赖项或复杂设置时。
例如:
late String description;
void setup() {
description = 'This will be initialized before use.';
}
在使用作为公共 API 一部分的 late
变量时要小心。如果客户端在变量初始化之前访问它,将会遇到 LateInitializationError
,它提供的上下文信息很少。在这种情况下,考虑使用一个私有的可空变量,并提供一个公共 getter,如果在访问过早时抛出描述性错误(例如 StateError
),这样可以为 API 用户提供更清晰的反馈,尽管会增加复杂性。
当变量只需要设置一次时,你也可以使用 late final
。这在对象构造时值不可用的场景中很有用,例如对象图中的循环依赖。
示例:
class LinkedQueue<T> {
late final QueueLink<T> _head;
LinkedQueue() {
_head = QueueLink<T>._head(owner: this); // Cyclic reference between objects
}
}
注意:如果在 late
变量初始化之前或完全未初始化的情况下访问它,将会导致运行时错误。
Mixin 应用
当一个 mixin 应用于一个类时创建的类。
Mixin 应用是当一个 mixin 应用于一个类时创建的类。例如,考虑以下声明:
class A {}
mixin M {}
class B extends A with M {}
类 B
是 Mixin M
应用于类 A
的 Mixin 应用(有时表示为 A+M
)的子类。类 A+M
是 A
的子类,并包含从 M
复制的成员。
你可以通过如下定义为 Mixin 应用赋予实际名称:
class A {}
mixin M {}
class A_M = A with M;
给定 A_M
的这个声明,以下对 B
的声明与原始示例中对 B
的声明是等效的:
class B extends A_M {}
相关文档与资源
覆盖推断
方法声明中缺失类型如何被推断。
覆盖推断是根据方法覆盖的一个或多个相应方法的类型来推断方法声明中任何缺失类型的过程。
如果候选方法(缺失类型信息的方法)覆盖了单个继承方法,则会推断出被覆盖方法中的相应类型。例如,考虑以下代码:
class A {
int m(String s) => 0;
}
class B extends A {
@override
m(s) => 1;
}
B
中 m
的声明是一个候选者,因为它同时缺少返回类型和参数类型。由于它覆盖了一个方法(A
中的方法 m
),因此将使用被覆盖方法中的类型来推断缺失的类型,就好像 B
中的方法被声明为 int m(String s) => 1;
一样。
如果候选方法覆盖了多个方法,并且其中一个被覆盖方法 Ms 的函数类型是所有其他被覆盖方法的函数类型的超类型,则使用 Ms 来推断缺失的类型。例如,考虑以下代码:
class A {
int m(num n) => 0;
}
class B {
num m(int i) => 0;
}
class C implements A, B {
@override
m(n) => 1;
}
C
中 m
的声明是覆盖推断的候选者,因为它同时缺少返回类型和参数类型。它覆盖了 A
中的 m
和 B
中的 m
,因此编译器需要选择其中一个来推断缺失的类型。但由于 A
中 m
的函数类型(int Function(num)
)是 B
中 m
的函数类型(num Function(int)
)的超类型,因此使用 A
中的函数来推断缺失的类型。结果与将 C
中的方法声明为 int m(num n) => 1;
相同。
如果没有被覆盖方法的函数类型是所有其他被覆盖方法的超类型,则会报错。
相关文档与资源
分部文件
包含 part of
指令的 Dart 源文件。
分部文件是包含 part of
指令并通过 part
指令包含在库中的 Dart 源文件。
潜在非空
明确为非空或由于是类型参数而为非空的类型。
一个类型是潜在非空的,如果它明确为非空,或者它是一个类型参数。
一个类型是明确非空的,如果它是后面没有问号 (?
) 的类型名称。请注意,有一些类型总是可空的,例如 Null
和 dynamic
,并且 FutureOr
只有在后面没有问号并且类型参数为非空时(例如 FutureOr<String>
)才为非空。
类型参数是潜在非空的,因为实际运行时类型(作为类型参数指定的类型)可能为非空。例如,给定 class C<T> {}
的声明,类型 C
可以与非空类型参数一起使用,如 C<int>
。
相关文档与资源
公共库
位于包的 lib
目录中,但不在 lib/src
目录内的库。
公共库是位于包的 lib
目录内但不在 lib/src
目录内的库。
相关文档与资源
重构
一种代码编辑,旨在进行非本地性修改或需要用户交互的修改。
重构是一种代码编辑,旨在进行非本地性修改或需要用户交互的修改。重构的例子包括重命名、删除或提取代码。
相关文档与资源
子类
继承另一个类实现的类。
子类是使用 extends
关键字或通过mixin 应用继承另一个类实现的类。
// A is a subclass of B; B is the superclass of A.
class A extends B {}
// B1 has the superclass `A with M`, which has the superclass A.
class B1 extends A with M {}
子类关系也意味着相关的子类型关系。例如,class A
隐式定义了一个关联类型 A
,类 A
的实例属于该类型。因此,class A extends B
不仅声明了类 A
是 B
的子类,还确立了类型 A
是类型 B
的子类型。
子类关系是子类型关系的子集。当文档说“S
必须是 T
的子类型”时,S
是 T
的子类是没问题的。然而,反过来不成立:并非所有子类型都是子类。
相关文档与资源
子类型
可以在任何需要其超类型值的地方使用的类型。
子类型关系是指在需要某种类型(超类型)值的地方,可以使用另一种类型的值进行替换。例如,如果 S
是 T
的子类型,则可以在需要类型 T
值的地方替换使用类型 S
的值。
子类型支持其超类型的所有操作(以及可能的一些额外操作)。实际上,这意味着你可以将子类型的值赋给任何期望超类型的位置,并且超类型的所有方法都可以在子类型上使用。
这至少在静态层面是成立的。特定的 API 在运行时可能不允许这种替换,这取决于其操作。
一些子类型关系基于类型的结构,例如可空类型(例如,int
是 int?
的子类型)和函数类型(例如,String Function()
是 void Function()
的子类型)。
// A is a subtype of B, but NOT a subclass of B.
class A implements B {}
// C is a subtype AND a subclass of D.
class C extends D {}
相关文档与资源
变型与变型位置
改变一个类型的类型参数如何影响原始类型和结果类型之间的关系。
在 Dart 中,改变类型声明(如类)或函数返回类型的类型参数,会使整体类型关系向同一方向变化(协变)。
然而,改变函数参数类型的类型,会使整体类型关系向相反方向变化(逆变)。
当一个类(或其他类型声明,如 mixin)的类型参数与实际类型参数“协变”时,该类型参数被称为协变的。换句话说,如果类型参数被替换为子类型,则整个类型也是子类型。
例如,类 List
的类型参数是协变的,因为列表类型与其类型参数协变:List<int>
是 List<Object>
的子类型,因为 int
是 Object
的子类型。
在 Dart 中,所有类、mixin、mixin class 和枚举声明的所有类型参数都是协变的。
然而,函数类型是不同的:函数类型在其返回类型上是协变的,但在其参数类型上是相反的(称为逆变)。例如,类型 int Function(int)
是类型 Object Function(int)
的子类型,但它是 int Function(Object)
的超类型。
如果你考虑它们的可替换性,这就说得通了。如果你调用一个静态类型为 int Function(int)
的函数,该函数在运行时实际上可以是类型 int Function(Object)
。根据静态类型,你期望能够传递一个 int
给它。这没有问题,因为该函数实际上接受任何 Object
,这包括所有类型为 int
的对象。类似地,返回的结果将是 int
类型,这也与你基于静态类型的期望一致。
因此,int Function(Object)
是 int Function(int)
的子类型。
请注意,对于参数类型来说,一切都颠倒了。特别是,函数类型之间的这种子类型关系要求参数类型存在相反的子类型关系。例如,void Function(Object)
是 void Function(int)
的子类型,因为 int
是 Object
的子类型。
对于像 List<void Function(int)>
这样更复杂的类型,你必须考虑类型中的位置。为了实现这一点,将类型的一部分变成一个占位符,然后考虑当将不同类型放置在该位置时,类型会发生什么变化。
例如,将 List<void Function(_)>
视为一个类型模板,你可以在占位符 _
的位置放入不同的类型。这种类型在该占位符出现的位置是逆变的。
以下通过将 Object
和 int
替换 _
来阐述这一点。List<void Function(Object)>
是 List<void Function(int)>
的子类型,因为 void Function(Object)
是 void Function(int)
的子类型,因为 void
是 void
的子类型(返回类型),并且 int
是 Object
的子类型(参数类型,顺序相反)。因此,位于 _
的类型与整体类型 List<void Function(_)>
的变化方向相反,而根据定义,这种“相反方向”使其成为一个逆变位置。
协变位置的定义类似。例如,_
在类型 List<_>
中处于协变位置,并且 _
在类型 _ Function(int)
中也处于协变位置。
还有另一种位置称为不变,但它出现的频率低得多,因此此处省略了其细节。
实际上,通常只需要知道类、mixin 等的类型参数处于协变位置,函数类型的返回类型也处于协变位置,而参数类型处于逆变位置就足够了。
通配符
在模式和其他上下文中使用的一个符号 (_
),用于代替变量名表示未使用的值。
通配符是下划线字符 (_
),用于忽略值或表明值是有意未使用的。它常用于模式、解构和 switch 表达式中,用于匹配任何值而不将其绑定到名称。
通配符通过明确标记在特定上下文不需要的值来帮助代码更具意图性。
示例:
// Ignoring the value in a for-each loop.
var names = ['Alice', 'Bob', 'Charlie'];
for (var _ in names) {
print('Someone is here!');
}
通配符模式在以下情况特别有用:
- 你只需要解构值中的某些部分。
- 你想明确表明某些值被忽略了。
- 你在模式匹配中需要一个捕获所有情况。
相关文档与资源
Zone
一种在不修改异步代码本身的情况下自定义异步代码行为的机制。
Zone 是一种执行上下文,允许你运行代码时自定义处理计时器、微任务和未捕获错误等异步事件的行为。
Zone 对于以下方面很有用:
- 日志记录
- 错误跟踪
- 在异步间隙中维护特定请求的状态(例如,在服务器应用中)
- 测试和调试异步行为
Zone 提供了一种跟踪和影响异步执行的方式,而无需异步代码了解 Zone 的存在。
你可以使用 runZoned
(或 runZonedGuarded
)创建一个新的 zone,并覆盖特定于 zone 的行为,例如错误处理和计时器。甚至 print
也可以被覆盖,尽管它不是异步的,只是为了方便而包含在内。
示例:
import 'dart:async';
void main() {
runZonedGuarded(() {
Future.delayed(Duration(seconds: 1), () {
throw 'Zone caught this error!';
});
}, (error, stackTrace) {
print('Caught error: $error');
});
}
在前面的示例中,异步回调内部的未捕获错误被自定义 zone 拦截。