一个简单的 Dart 程序

新建一个dart文件demo.dart

  1. // Define a function.
  2. void printInteger(int aNumber) {
  3. print('The number is $aNumber.'); // Print to console.
  4. }
  5. // This is where the app starts executing.
  6. void main() {
  7. var number = 42; // Declare and initialize a variable.
  8. printInteger(number); // Call a function.
  9. }

说明:

  • 注释。以双斜杠开头的一行语句称为单行注释。Dart 同样支持多行注释和文档注释。
  • void。一种数据类型,表示空值。
  • int。一种数据类型,表示一个整型数字。
  • 42。表示一个数字字面量。数字字面量是一种编译时常量。
  • print()。一种便利的将信息输出显示的方式。
  • $variableName (或 ${expression})。表示变量或表达式。
  • main()。一个特殊且 必须的 顶级函数,Dart 应用程序总是会从该函数开始执行。
  • var。用于定义变量,通过这种方式定义变量不需要指定变量类型。这类变量的类型 (int) 由它的初始值决定 (42)

重要概念

  • 所有变量引用的都是 对象,每个对象都是一个 的实例。数字、函数以及 null 都是对象。除去 null 以外(如果你开启了空安全,Dart 2.12 引入), 所有的类都继承于 Object 类。

  • 尽管 Dart 是强类型语言,但是在声明变量时指定类型是可选的,因为 Dart 可以进行类型推断。在上述代码中,变量 number 的类型被推断为 int 类型。

  • 如果你开启了 空安全,变量在未声明为可空类型时不能为 null。你可以通过在类型后加上问号 (?) 将类型声明为可空。例如,int? 类型的变量可以是整形数字或 null。如果你 明确知道 一个表达式不会为空,但 Dart 不这么认为时,你可以在表达式后添加 ! 来断言表达式不为空(为空时将抛出异常)。例如:int x = nullableButNotNullInt!

  • 如果你想要显式地声明允许任意类型,使用 Object?(如果你 开启了空安全)、 Object 或者 特殊类型 dynamic 将检查延迟到运行时进行。

  • Dart 支持泛型,比如 List<int>(表示一组由 int 对象组成的列表)或 List<Object>(表示一组由任何类型对象组成的列表)。

  • Dart 支持顶级函数(例如 main 方法),同时还支持定义属于类或对象的函数(即 静态 和 实例方法)。你还可以在函数中定义函数(嵌套 或 局部函数)。

  • Dart 支持顶级 变量,以及定义属于类或对象的变量(静态和实例变量)。实例变量有时称之为域或属性。

  • Dart 没有类似于 Java 那样的 publicprotectedprivate 成员访问限定符。如果一个标识符以下划线 (_) 开头则表示该标识符在库内是私有的。

  • 标识符 可以以字母或者下划线 (_) 开头,其后可跟字符和数字的组合。

  • Dart 中 表达式 和 语句 是有区别的,表达式有值而语句没有。比如条件表达式 expression condition ? expr1 : expr2 中含有值 expr1 或 expr2。与 if-else 分支语句相比,if-else 分支语句则没有值。一个语句通常包含一个或多个表达式,但是一个表达式不能只包含一个语句。

  • Dart 工具可以显示 警告错误 两种类型的问题。警告表明代码可能有问题但不会阻止其运行。错误分为编译时错误和运行时错误;编译时错误代码无法运行;运行时错误会在代码运行时导致异常。

关键字

分为4类:

  • 上下文关键字,只有在特定的场景才有意义,它们可以在任何地方作为有效的标识符。
  • 内置标识符,这些关键字在大多数时候都可以作为有效的标识符,但是它们不能用作类名或者类型名或者作为导入前缀使用
  • 异步相关的标识符。不能在由关键字 asyncasync*sync* 标识的方法体中使用 awaityield 作为标识符。
  • 保留字,不能用作标识符

变量

下面的示例代码将创建一个变量并将其初始化:

  1. var name = 'Bob';

变量仅存储对象的引用。这里名为 name 的变量存储了一个 String 类型对象的引用,“Bob” 则是该对象的值。

name 变量的类型被推断为 String,但是你可以为其指定类型。如果一个对象的引用不局限于单一的类型,可以将其指定为 Object(或 dynamic)类型。

  1. Object name = 'Bob';

除此之外你也可以指定类型:

  1. String name = 'Bob';

风格建议指南中的建议,通过 var 声明局部变量而非使用指定的类型。

默认值

在 Dart 中,未初始化以及可空类型的变量拥有一个默认的初始值 null。(如果你未迁移至 空安全,所有变量都为可空类型。)即便数字也是如此,因为在 Dart 中一切皆为对象,数字也不例外。

  1. int? lineCount;
  2. assert(lineCount == null);

assert() 的调用将会在生产环境的代码中被忽略掉。在开发过程中,assert(*condition*) 将会在 条件判断 为 false 时抛出一个异常。

如果启用了空安全,则变量声明时必须赋值

  1. int lineCount = 0;

你不必在声明时初始化一个局部变量,但是你必须在使用这个变量之前给他赋予一个值。例如:

  1. int lineCount;
  2. if (weLikeToCount) {
  3. lineCount = countLines();
  4. } else {
  5. lineCount = 0;
  6. }
  7. print(lineCount);

顶级变量和类变量是延时初始化的,初始化的代码是在变量使用之前执行的。

Late变量

Dart 2.12 添加 Late 修饰符,主要用于以下两种场景:

  • 声明一个非空的变量,它是在声明之后进行初始化。
  • 延迟初始化一个变量。

通常Dart的控制流分析可以在使用非空变量之前检测到该变量是否被设置为非空值,但有时分析会失败。两种常见的情况是顶级变量和实例变量:Dart通常不能确定是否设置了它们,所以它不会尝试。
如果你确定一个变量在使用之前就被设置了,但是Dart不同意,你可以通过标记变量为late来解决这个错误:

  1. late String description;
  2. void main() {
  3. description = 'Feijoada!';
  4. print(description);
  5. }

如果late延迟初始化变量失败,则在使用该变量时发生运行时错误。

如果将一个变量标记为late,但在声明时初始化它,那么初始化式将在该变量第一次使用时运行。这种延迟初始化在以下几种情况下很方便:

  • 该变量可能不需要,并且初始化它的代价很高。
  • 你正在初始化一个实例变量,它的初始化需要访问这个。

在下面的例子中,如果从未使用过temperature变量,则不会调用_readThermometer()函数:

  1. // This is the program's only call to _readThermometer().
  2. late String temperature = _readThermometer(); // Lazily initialized.

Final 和 Const

如果你不想更改一个变量,可以使用关键字 final 或者 const 修饰变量,这两个关键字可以替代 var 关键字或者加在一个具体的类型前。一个 final 变量只可以被赋值一次;一个 const 变量是一个编译时常量(const 变量同时也是 final 的)。顶层的 final 变量或者类的 final 变量在其第一次使用的时候被初始化。

虽然final对象不能被修改,但是它的字段可以被修改。相比之下,const对象及其字段是不可更改的:它们是不可变的。

实例变量可以是 final 的但不可以是 const

下面的示例中我们创建并设置两个 final 变量:

  1. final name = 'Bob'; // 没有类型注释
  2. final String nickname = 'Bobby';

你不能修改 final 变量的值:

  1. name = 'Alice'; // Error: final变量只能设置一次.

使用关键字 const 修饰变量表示该变量为 编译时常量。如果使用 const 修饰类中的变量,则必须加上 static 关键字,即 static const(译者注:顺序不能颠倒)。在声明 const 变量时可以直接为其赋值,也可以使用其它的 const 变量为其赋值:

  1. const bar = 1000000; // Unit of pressure (dynes/cm2)
  2. const double atm = 1.01325 * bar; // Standard atmosphere

const 关键字不仅仅可以用来定义常量,还可以用来创建 常量值,该常量值可以赋予给任何变量。你也可以将构造函数声明为 const 的,这种类型的构造函数创建的对象是不可改变的。

  1. var foo = const [];
  2. final bar = const [];
  3. const baz = []; // Equivalent to `const []`

如果使用初始化表达式为常量赋值可以省略掉关键字 const,比如上面的常量 baz 的赋值就省略掉了 const

没有使用 final 或 const 修饰的变量的值是可以被更改的,即使这些变量之前引用过 const 的值。

  1. foo = [1, 2, 3]; // Was const []

常量的值不可以被修改:

  1. baz = [42]; // Error: 常量变量不能被赋值.

你可以在常量中使用 类型检查和强制类型转换 (isas)、 集合中的 if以及 展开操作符 (......?):

  1. const Object i = 3; // Where i is a const Object with an int value...
  2. const list = [i as int]; // Use a typecast.
  3. const map = {if (i is int) i: 'int'}; // Use is and collection if.
  4. const set = {if (list is List<int>) ...list}; // ...and a spread.

内置类型

Dart 语言支持下列类型:

  • Numbers (int, double)
  • Strings (String)
  • Booleans (bool)
  • Lists
  • Sets(Set)
  • Maps (Map)
  • Runes
  • Symbols (Symbol)
  • null (Null)

使用字面量来创建对象也受到支持。例如 'This is a string' 是一个字符串字面量,true 是一个布尔字面量。

由于 Dart 中每个变量引用都指向一个对象(一个 的实例),通常也可以使用 构造器 来初始化变量。一些内置的类型有它们自己的构造器。例如你可以使用 Map() 来创建一个 map 对象。

其他类型在Dart语言中也有特殊的作用:

  • Object:所有Dart类的超类,除了Null。
  • Future和Stream:用于异步支持。
  • Iterable:用于for-in循环和同步生成器函数中。
  • Never:表示表达式永远不能成功地完成求值。最常用于总是抛出异常的函数。
  • dynamic:关闭静态检查。通常你应该使用Object或Object?代替。
  • void:表示某个值从未被使用。通常用作返回类型。

Number

Dart 支持两种 Number 类型:

  • int
    整数值;长度不超过 64 位,具体取值范围 依赖于不同的平台。在 DartVM 上其取值位于 -263 至 263 - 1 之间。在 Web 上,整型数值代表着 JavaScript 的数字(64 位无小数浮点型),其允许的取值范围在 -253 至 253 - 1 之间。

  • double
    64 位的双精度浮点数字,且符合 IEEE 754 标准。

intdouble 都是 [num][num] 的子类。 num 中定义了一些基本的运算符比如 +、-、*、/ 等,还定义了 abs()ceil()floor() 等方法(位运算符,比如 >> 定义在 int 中)。如果 num 及其子类不满足你的要求,可以查看 dart:math 库中的 API。

整数是不带小数点的数字,下面是一些定义整数字面量的例子:

  1. var x = 1;
  2. var hex = 0xDEADBEEF;
  3. var exponent = 8e5;

如果一个数字包含了小数点,那么它就是浮点型的。下面是一些定义浮点数字面量的例子:

  1. var y = 1.1;
  2. var exponents = 1.42e5;

还可以将变量声明为num,如果您这样做,该变量可以同时具有整型和双精度值

  1. num x = 1; // x可以同时具有int和double值
  2. x += 2.5;

整型字面量将会在必要的时候自动转换成浮点数字面量:

  1. double z = 1; // 相当于 double z = 1.0.

在 Dart 2.1 之前,在浮点数上下文中使用整数字面量是错误的。

String

Dart 字符串(String 对象)包含了 UTF-16 编码的字符序列。可以使用单引号或者双引号来创建字符串:

  1. var s1 = 'Single quotes work well for string literals.';
  2. var s2 = "Double quotes work just as well.";
  3. var s3 = 'It's easy to escape the string delimiter.';
  4. var s4 = "It's even easier to use the other delimiter.";

在字符串中,请以 ${表达式} 的形式使用表达式,如果表达式是一个标识符,可以省略掉 {}。如果表达式的结果为一个对象,则 Dart 会调用该对象的 toString 方法来获取一个字符串。

  1. // 代码解释
  2. var s = '字符串插值';
  3. assert('Dart 有$s,使用起来非常方便。' == 'Dart 有字符串插值,使用起来非常方便。');
  4. assert('使用${s.substring(3,5)}表达式也非常方便' == '使用插值表达式也非常方便。');

== 运算符负责判断两个对象的内容是否一样,如果两个字符串包含一样的字符编码序列,则表示相等。
你可以使用 + 运算符或并列放置多个字符串来连接字符串:

  1. // 代码解释
  2. var s1 = '可以拼接'
  3. '字符串'
  4. "即便它们不在同一行。";
  5. print(s1 == '可以拼接字符串即便它们不在同一行。');
  6. var s2 = '使用加号 + 运算符' + '也可以达到相同的效果。';
  7. print(s2 == '使用加号 + 运算符也可以达到相同的效果。');

使用三个单引号或者三个双引号也能创建多行字符串:

  1. // 代码解释
  2. var s5 = '''你可以像这样创建多行字符串''';
  3. var s6 = """你可以像这样创建多行字符串""";
  4. print(s5 == s6); // true

在字符串前加上 r 作为前缀创建 “raw” 字符串(即不会被做任何处理(比如转义)的字符串):

  1. var s = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。';
  2. print(s); // 在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。\

你可以查阅 Runes 与 grapheme clusters 获取更多关于如何在字符串中表示 Unicode 字符的信息。

字符串字面量是一个编译时常量,只要是编译时常量 (null、数字、字符串、布尔) 都可以作为字符串字面量的插值表达式:

  1. // These work in a const string.
  2. const aConstNum = 0;
  3. const aConstBool = true;
  4. const aConstString = 'a constant string';
  5. // These do NOT work in a const string.
  6. var aNum = 0;
  7. var aBool = true;
  8. var aString = 'a string';
  9. const aConstList = [1, 2, 3];
  10. const validConstString = '$aConstNum $aConstBool $aConstString';
  11. // const invalidConstString = '$aNum $aBool $aString $aConstList';

可以查阅 字符串和正则表达式 获取更多关于如何使用字符串的信息。

布尔类型

Dart 使用 bool 关键字表示布尔类型,布尔类型只有两个对象 truefalse,两者都是编译时常量。

Dart 的类型安全不允许你使用类似 if (nonbooleanValue) 或者 assert (nonbooleanValue) 这样的代码检查布尔值。相反,你应该总是显示地检查布尔值,比如像下面的代码这样:

  1. // 检查空字符串。
  2. var fullName = '';
  3. print(fullName.isEmpty); // true
  4. // 检查 0 值。
  5. var hitPoints = 0;
  6. print(hitPoints <= 0); // true
  7. // 检查 null 值。
  8. var unicorn;
  9. print(unicorn == null); // true
  10. // 检查 NaN 。
  11. var iMeantToDoThis = 0 / 0;
  12. print(iMeantToDoThis.isNaN); // true

List

数组 (Array) 是几乎所有编程语言中最常见的集合类型,在 Dart 中数组由 List 对象表示。通常称之为 List

Dart 中 List 字面量看起来与 JavaScript 中数组字面量一样。下面是一个 Dart List 的示例:

  1. var list = [1, 2, 3];

这里 Dart 推断出 list 的类型为 List<int>,如果往该数组中添加一个非 int 类型的对象则会报错。

你可以在 Dart 的集合类型的最后一个项目后添加逗号。这个尾随逗号并不会影响集合,但它能有效避免「复制粘贴」的错误。

  1. var list = ['Car','Boat','Plane',];

List 的下标索引从 0 开始,第一个元素的下标为 0,最后一个元素的下标为 list.length - 1。你可以像 JavaScript 中的用法那样获取 Dart 中 List 的长度以及元素:

  1. var list = [1, 2, 3];
  2. assert(list.length == 3);
  3. assert(list[1] == 2);
  4. list[1] = 1;
  5. assert(list[1] == 1);

在 List 字面量前添加 const 关键字会创建一个编译时常量:

  1. var constantList = const [1, 2, 3];
  2. // constantList[1] = 1; // This line will cause an error.

创建final实例变量,final对象不能被修改,但是它的字段可以被修改:

  1. final list = [1, 2, 3];
  2. var constantList = list;
  3. constantList[1] = 1;
  4. print(constantList); // [1, 1, 3]

Dart 在 2.3 引入了 扩展操作符...)和 空感知扩展操作符...?),它们提供了一种将多个元素插入集合的简洁方法。
你可以使用扩展操作符(...)将一个 List 中的所有元素插入到另一个 List 中:

  1. var list = [1, 2, 3];
  2. var list2 = [0, ...list];
  3. assert(list2.length == 4);

如果扩展操作符右边可能为 null ,你可以使用 null-aware 扩展操作符(...?)来避免产生异常:

  1. var list;
  2. var list2 = [0, ...?list];
  3. assert(list2.length == 1);

Dart 还同时引入了 集合中的 if集合中的 for 操作,在构建集合时,可以使用条件判断 (if) 和循环 (for)。

下面示例是使用 集合中的 if 来创建一个 List 的示例,它可能包含 3 个或 4 个元素:

  1. var nav = [
  2. 'Home',
  3. 'Furniture',
  4. 'Plants',
  5. if (promoActive) 'Outlet'
  6. ];

下面是使用 集合中的 for 将列表中的元素修改后添加到另一个列表中的示例:

  1. var listOfInts = [1, 2, 3];
  2. var listOfStrings = [
  3. '#0',
  4. for (var i in listOfInts) '#$i'
  5. ];
  6. assert(listOfStrings[1] == '#1');

你可以查阅 集合中使用控制流建议 获取更多关于在集合中使用 iffor 的细节内容和示例。

List 类中有许多用于操作 List 的便捷方法,你可以查阅 泛型集合 获取更多与之相关的信息。

Set

在 Dart 中,set 是一组元素唯一的无序集合。 Dart 支持的集合由集合的字面量和 Set 类提供。

尽管 Set 类型(type) 一直都是 Dart 的一项核心功能,但是 Set 字面量(literals) 是在 Dart 2.2 中才加入的。

下面是使用 Set 字面量来创建一个 Set 集合的方法:

  1. var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

Dart 推断 halogens 变量是一个 Set<String> 类型的集合,如果往该 Set 中添加类型不正确的对象则会报错。你可以查阅 类型推断 获取更多与之相关的内容。

可以使用在 {} 前加上类型参数的方式创建一个空的 Set,或者将 {} 赋值给一个 Set 类型的变量:

  1. var names = <String>{};
  2. // Set<String> names = {}; // 这样也是可以的。
  3. // var names = {}; // 这样会创建一个 Map ,而不是 Set 。

Set 还是 map? Map 字面量语法相似于 Set 字面量语法。因为先有的 Map 字面量语法,所以 {} 默认是 Map 类型。如果忘记在 {} 上注释类型或赋值到一个未声明类型的变量上,那么 Dart 会创建一个类型为 Map<dynamic, dynamic> 的对象。

使用 add() 方法或 addAll() 方法向已存在的 Set 中添加项目:

  1. var elements = <String>{};
  2. elements.add('fluorine');
  3. elements.addAll(halogens);

使用 .length 可以获取 Set 中元素的数量:

  1. var elements = <String>{};
  2. elements.add('fluorine');
  3. elements.addAll(halogens);
  4. assert(elements.length == 5);

可以在 Set 变量前添加 const 关键字创建一个 Set 编译时常量:

  1. final constantSet = const {
  2. 'fluorine',
  3. 'chlorine',
  4. 'bromine',
  5. 'iodine',
  6. 'astatine',
  7. };
  8. // constantSet.add('helium'); // This line will cause an error.

从 Dart 2.3 开始,Set 可以像 List 一样支持使用扩展操作符(......?)以及 Collection iffor 操作。你可以查阅 List 扩展操作符List 集合操作符 获取更多相关信息。

你也可以查阅 泛型 以及 Set 获取更多相关信息。

Map

通常来说,Map 是用来关联 keys 和 values 的对象。其中键和值都可以是任何类型的对象。每个 只能出现一次但是 可以重复出现多次。 Dart 中 Map 提供了 Map 字面量以及 Map 类型两种形式的 Map。

下面是一对使用 Map 字面量创建 Map 的例子:

  1. var gifts = {
  2. // Key: Value
  3. 'first': 'partridge',
  4. 'second': 'turtledoves',
  5. 'fifth': 'golden rings'
  6. };
  7. var nobleGases = {
  8. 2: 'helium',
  9. 10: 'neon',
  10. 18: 'argon',
  11. };

Dart 将 gifts 变量的类型推断为 Map<String, String>,而将 nobleGases 的类型推断为 Map<int, String>。如果你向这两个 Map 对象中添加不正确的类型值,将导致运行时异常。你可以阅读 类型推断 获取更多相关信息。

你也可以使用 Map 的构造器创建 Map:

  1. var gifts = Map<String, String>();
  2. gifts['first'] = 'partridge';
  3. gifts['second'] = 'turtledoves';
  4. gifts['fifth'] = 'golden rings';
  5. var nobleGases = Map<int, String>();
  6. nobleGases[2] = 'helium';
  7. nobleGases[10] = 'neon';
  8. nobleGases[18] = 'argon';

如果你之前是使用的 C# 或 Java 这样的语言,也许你想使用 new Map() 构造 Map 对象。但是在 Dart 中,new 关键词是可选的。(且不被建议使用) 你可以查阅 构造函数的使用 获取更多相关信息。

向现有的 Map 中添加键值对与 JavaScript 的操作类似:

  1. var gifts = {'first': 'partridge'};
  2. gifts['fourth'] = 'calling birds'; // Add a key-value pair

从一个 Map 中获取一个值的操作也与 JavaScript 类似:

  1. var gifts = {'first': 'partridge'};
  2. assert(gifts['first'] == 'partridge');

如果检索的 Key 不存在于 Map 中则会返回一个 null:

  1. var gifts = {'first': 'partridge'};
  2. assert(gifts['fifth'] == null);

使用 .length 可以获取 Map 中键值对的数量:

  1. var gifts = {'first': 'partridge'};
  2. gifts['fourth'] = 'calling birds';
  3. assert(gifts.length == 2);

在一个 Map 字面量前添加 const 关键字可以创建一个 Map 编译时常量:

  1. final constantMap = const {
  2. 2: 'helium',
  3. 10: 'neon',
  4. 18: 'argon',
  5. };
  6. // constantMap[2] = 'Helium'; // This line will cause an error.

Map 可以像 List 一样支持使用扩展操作符(......?)以及集合的 if 和 for 操作。你可以查阅 List 扩展操作符List 集合操作符 获取更多相关信息。

你也可以查阅 泛型 以及 Maps API 获取更多相关信息。

Runes 与 grapheme clusters

在 Dart 中,runes 公开了字符串的 Unicode 码位。使用 characters 包 来访问或者操作用户感知的字符,也被称为 Unicode (扩展) grapheme clusters

Unicode 编码为每一个字母、数字和符号都定义了一个唯一的数值。因为 Dart 中的字符串是一个 UTF-16 的字符序列,所以如果想要表示 32 位的 Unicode 数值则需要一种特殊的语法。

表示 Unicode 字符的常见方式是使用 \uXXXX,其中 XXXX 是一个四位数的 16 进制数字。例如心形字符(♥)的 Unicode 为 \u2665。对于不是四位数的 16 进制数字,需要使用大括号将其括起来。例如大笑的 emoji 表情(😆)的 Unicode 为 \u{1f600}

如果你需要读写单个 Unicode 字符,可以使用 characters 包中定义的 characters getter。它将返回 Characters 对象作为一系列 grapheme clusters 的字符串。下面是使用 characters API 的样例:

  1. import 'package:characters/characters.dart';
  2. ...
  3. var hi = 'Hi 🇩🇰';
  4. print(hi);
  5. print('The end of the string: ${hi.substring(hi.length - 1)}');
  6. print('The last character: ${hi.characters.last}\n');

输出取决于你的环境,大致类似于:

  1. $ dart run bin/main.dart
  2. Hi 🇩🇰
  3. The end of the string: ???
  4. The last character: 🇩🇰

有关使用 characters 包操作字符串的详细信息,请参阅用于 characters 包的样例API 参考

在使用 List 操作 Rune 的时候需要小心,根据所操作的语种、字符集等不同可能会导致字符串出现问题,具体可参考 Stack Overflow 中的提问: [我如何在 Dart 中反转一个字符串?][How do I reverse a String in Dart?]。

Symbol

Symbol 表示 Dart 中声明的操作符或者标识符。你几乎不会需要 Symbol,但是它们对于那些通过名称引用标识符的 API 很有用,因为代码压缩后,尽管标识符的名称会改变,但是它们的 Symbol 会保持不变。

可以使用在标识符前加 # 前缀来获取 Symbol:

  1. #radix
  2. #bar

Symbol 字面量是编译时常量。

函数

Dart 是一种真正面向对象的语言,所以即便函数也是对象并且类型为 Function,这意味着函数可以被赋值给变量或者作为其它函数的参数。你也可以像调用函数一样调用 Dart 类的实例。

下面是定义一个函数的例子:

  1. bool isNoble(int atomicNumber) {
  2. return _nobleGases[atomicNumber] != null;
  3. }

虽然高效 Dart 指南建议在公开的 API 上定义返回类型,不过即便不定义,该函数也依然有效:

  1. isNoble(atomicNumber) {
  2. return _nobleGases[atomicNumber] != null;
  3. }

如果函数体内只包含一个表达式,你可以使用简写语法:

  1. bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

语法 => *表达式*{ return *表达式*; } 的简写, => 有时也称之为 箭头 函数。

在 => 与 ; 之间的只能是 表达式 而非 语句。比如你不能将一个if语句放在其中,但是可以放置条件表达式。

参数

函数可以有两种形式的参数:必要参数可选参数。必要参数定义在参数列表前面,可选参数则定义在必要参数后面。可选参数可以是 命名的位置的

向函数传入参数或者定义函数参数时,可以使用 尾逗号

命名参数

命名参数默认为可选参数,除非他们被特别标记为 required

当你调用函数时,可以使用 *参数名*: *参数值* 的形式来指定命名参数。例如:

  1. enableFlags(bold: true, hidden: false);

定义函数时,使用 {*参数1*, *参数2*, …} 来指定命名参数:

  1. /// Sets the [bold] and [hidden] flags ...
  2. void enableFlags({bool? bold, bool? hidden}) {...}

虽然命名参数是可选参数的一种类型,但是你仍然可以使用 required 来标识一个命名参数是必须的参数,此时调用者必须为该参数提供一个值。例如:

  1. const Scrollbar({Key? key, required Widget child})

如果调用者想要通过 Scrollbar 的构造函数构造一个 Scrollbar 对象而不提供 child 参数,则会导致编译错误。

可选的位置参数

使用 [] 将一系列参数包裹起来作为位置参数:

  1. String say(String from, String msg, [String? device]) {
  2. var result = '$from says $msg';
  3. if (device != null) {
  4. result = '$result with a $device';
  5. }
  6. return result;
  7. }

下面是不使用可选参数调用上述函数的示例

  1. assert(say('Bob', 'Howdy') == 'Bob says Howdy');

下面是使用可选参数调用上述函数的示例:

  1. assert(say('Bob', 'Howdy', 'smoke signal') ==
  2. 'Bob says Howdy with a smoke signal');

默认参数值

可以用 = 为函数的命名参数和位置参数定义默认值,默认值必须为编译时常量,没有指定默认值的情况下默认值为 null

下面是设置可选参数默认值示例:

  1. /// Sets the [bold] and [hidden] flags ...
  2. void enableFlags({bool bold = false, bool hidden = false}) {...}
  3. // bold will be true; hidden will be false.
  4. enableFlags(bold: true);

在老版本的 Dart 代码中会使用冒号(:)而不是 = 来设置命名参数的默认值。原因在于刚开始的时候命名参数只支持 :。不过现在这个支持已经过时,所以我们建议你现在仅 使用 = 来指定默认值

下一个示例将向你展示如何为位置参数设置默认值:

  1. String say(String from, String msg,
  2. [String device = 'carrier pigeon']) {
  3. var result = '$from says $msg with a $device';
  4. return result;
  5. }
  6. assert(say('Bob', 'Howdy') ==
  7. 'Bob says Howdy with a carrier pigeon');

List 或 Map 同样也可以作为默认值。下面的示例定义了一个名为 doStuff() 的函数,并为其名为 listgifts 的参数指定了一个 List 类型的值和 Map 类型的值。

  1. void doStuff(
  2. {List<int> list = const [1, 2, 3],
  3. Map<String, String> gifts = const {
  4. 'first': 'paper',
  5. 'second': 'cotton',
  6. 'third': 'leather'
  7. }}) {
  8. print('list: $list');
  9. print('gifts: $gifts');
  10. }

main() 函数

每个 Dart 程序都必须有一个 main() 顶级函数作为程序的入口, main() 函数返回值为 void 并且有一个 List<String> 类型的可选参数。

下面是一个简单 main() 函数:

  1. void main() {
  2. print('Hello, World!');
  3. }

下面是使用命令行访问带参数的 main() 函数示例:

  1. // Run the app like this: dart args.dart 1 test
  2. void main(List<String> arguments) {
  3. print(arguments);
  4. assert(arguments.length == 2);
  5. assert(int.parse(arguments[0]) == 1);
  6. assert(arguments[1] == 'test');
  7. }

你可以通过使用 参数库 来定义和解析命令行参数。

函数是一级对象

可以将函数作为参数传递给另一个函数。例如:

  1. void printElement(int element) {
  2. print(element);
  3. }
  4. var list = [1, 2, 3];
  5. // Pass printElement as a parameter.
  6. list.forEach(printElement);

你也可以将函数赋值给一个变量,比如:

  1. var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
  2. assert(loudify('hello') == '!!! HELLO !!!');

该示例中使用了匿名函数。下一节会有更多与其相关的介绍。

匿名函数

大多数方法都是有名字的,比如 main()printElement()。你可以创建一个没有名字的方法,称之为 匿名函数Lambda 表达式Closure 闭包。你可以将匿名方法赋值给一个变量然后使用它,比如将该变量添加到集合或从中删除。

匿名方法看起来与命名方法类似,在括号之间可以定义参数,参数之间用逗号分割。

后面大括号中的内容则为函数体:

  1. ([[*类型*] *参数*[, …]]) { *函数体*;};

下面代码定义了只有一个参数 item 且没有参数类型的匿名方法。 List 中的每个元素都会调用这个函数,打印元素位置和值的字符串:

  1. const list = ['apples', 'bananas', 'oranges'];
  2. list.forEach((item) {
  3. print('${list.indexOf(item)}: $item');
  4. });

如果函数体内只有一行返回语句,你可以使用胖箭头缩写法。

  1. list.forEach(
  2. (item) => print('${list.indexOf(item)}: $item'));

词法作用域

Dart 是词法有作用域语言,变量的作用域在写代码的时候就确定了,大括号内定义的变量只能在大括号内访问,与 Java 类似。

下面是一个嵌套函数中变量在多个作用域中的示例:

  1. bool topLevel = true;
  2. void main() {
  3. var insideMain = true;
  4. void myFunction() {
  5. var insideFunction = true;
  6. void nestedFunction() {
  7. var insideNestedFunction = true;
  8. assert(topLevel);
  9. assert(insideMain);
  10. assert(insideFunction);
  11. assert(insideNestedFunction);
  12. }
  13. }
  14. }

注意 nestedFunction() 函数可以访问包括顶层变量在内的所有的变量。

词法闭包

闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。

函数可以封闭定义到它作用域内的变量。接下来的示例中,函数 makeAdder() 捕获了变量 addBy。无论函数在什么时候返回,它都可以使用捕获的 addBy 变量。

  1. /// Returns a function that adds [addBy] to the
  2. /// function's argument.
  3. Function makeAdder(int addBy) {
  4. return (int i) => addBy + i;
  5. }
  6. void main() {
  7. // Create a function that adds 2.
  8. var add2 = makeAdder(2);
  9. // Create a function that adds 4.
  10. var add4 = makeAdder(4);
  11. assert(add2(3) == 5);
  12. assert(add4(3) == 7);
  13. }

测试函数是否相等

下面是顶级函数,静态方法和示例方法相等性的测试示例:

  1. void foo() {} // A top-level function
  2. class A {
  3. static void bar() {} // A static method
  4. void baz() {} // An instance method
  5. }
  6. void main() {
  7. Function x;
  8. // Comparing top-level functions.
  9. x = foo;
  10. assert(foo == x);
  11. // Comparing static methods.
  12. x = A.bar;
  13. assert(A.bar == x);
  14. // Comparing instance methods.
  15. var v = A(); // Instance #1 of A
  16. var w = A(); // Instance #2 of A
  17. var y = w;
  18. x = w.baz;
  19. // These closures refer to the same instance (#2),
  20. // so they're equal.
  21. assert(y.baz == x);
  22. // These closures refer to different instances,
  23. // so they're unequal.
  24. assert(v.baz != w.baz);
  25. }

返回值

所有的函数都有返回值。没有显示返回语句的函数最后一行默认为执行 return null;

  1. foo() {}
  2. assert(foo() == null);

运算符

Dart 支持下表的操作符。你可以将这些运算符实现为 一个类的成员

一旦你使用了运算符,就创建了表达式。下面是一些运算符表达式的示例:

  1. a++
  2. a + b
  3. a = b
  4. a == b
  5. c ? a : b
  6. a is T

运算符表 中,运算符的优先级按先后排列,即第一行优先级最高,最后一行优先级最低,而同一行中,最左边的优先级最高,最右边的优先级最低。例如:% 运算符优先级高于 == ,而 == 高于 &&。根据优先级规则,那么意味着以下两行代码执行的效果相同:

  1. // Parentheses improve readability.
  2. if ((n % i == 0) && (d % i == 0)) ...
  3. // Harder to read, but equivalent.
  4. if (n % i == 0 && d % i == 0) ...

请注意:

对于有两个操作数的运算符,左边的操作数决定了运算符的功能。比如对于一个 Vector 对象和一个 Point 对象,表达式 aVector + aPoint 中所使用的是 Vector 对象中定义的相加运算符 (+)。

算术运算符

Dart 支持常用的算术运算符:

运算符 描述
+
-*表达式* 一元负, 也可以作为反转(反转表达式的符号)
*
/
~/ 除并取整
% 取模

示例:

  1. assert(2 + 3 == 5);
  2. assert(2 - 3 == -1);
  3. assert(2 * 3 == 6);
  4. assert(5 / 2 == 2.5); // Result is a double
  5. assert(5 ~/ 2 == 2); // Result is an int
  6. assert(5 % 2 == 1); // Remainder
  7. assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 还支持自增自减操作。

Operator++*var* *var* = *var* + 1 (表达式的值为 *var* + 1)
*var*++ *var* = *var* + 1 (表达式的值为 *var*)
--*var* *var* = *var* – 1 (表达式的值为 *var* – 1)
*var*-- *var* = *var* – 1 (表达式的值为 *var*)

示例:

  1. int a;
  2. int b;
  3. a = 0;
  4. b = ++a; // Increment a before b gets its value.
  5. assert(a == b); // 1 == 1
  6. a = 0;
  7. b = a++; // Increment a AFTER b gets its value.
  8. assert(a != b); // 1 != 0
  9. a = 0;
  10. b = --a; // Decrement a before b gets its value.
  11. assert(a == b); // -1 == -1
  12. a = 0;
  13. b = a--; // Decrement a AFTER b gets its value.
  14. assert(a != b); // -1 != 0

关系运算符

下表列出了关系运算符及含义:

Operator== 相等
!= 不等
> 大于
< 小于
>= 大于等于
<= 小于等于

要判断两个对象 x 和 y 是否表示相同的事物使用 == 即可。(在极少数情况下,可能需要使用 identical() 函数来确定两个对象是否完全相同)。下面是 == 运算符的一些规则:

  1. xy 同时为空时返回 true,而只有一个为空时返回 false。
  2. 返回对 x 调用 == 方法的结果,参数为 y。(像 == 这样的操作符是对左侧内容进行调用的。详情请查阅 操作符。)

下面的代码给出了每一种关系运算符的示例:

  1. assert(2 == 2);
  2. assert(2 != 3);
  3. assert(3 > 2);
  4. assert(2 < 3);
  5. assert(3 >= 3);
  6. assert(2 <= 3);

类型判断运算符

asisis! 运算符是在运行时判断对象类型的运算符。

Operator Meaning
as 类型转换(也用作指定 类前缀))
is 如果对象是指定类型则返回 true
is! 如果对象是指定类型则返回 false

当且仅当 obj 实现了 T 的接口,obj is T 才是 true。例如 obj is Object 总为 true,因为所有类都是 Object 的子类。

仅当你确定这个对象是该类型的时候,你才可以使用 as 操作符可以把对象转换为特定的类型。例如:

  1. (employee as Person).firstName = 'Bob';

如果你不确定这个对象类型是不是 T,请在转型前使用 is T 检查类型。

  1. if (employee is Person) {
  2. // Type check
  3. employee.firstName = 'Bob';
  4. }

备忘:

上述两种方式是有区别的:如果 employee 为 null 或者不为 Person 类型,则第一种方式将会抛出异常,而第二种不会。

赋值运算符

可以使用 = 来赋值,同时也可以使用 ??= 来为值为 null 的变量赋值。

  1. // Assign value to a
  2. a = value;
  3. // Assign value to b if b is null; otherwise, b stays the same
  4. b ??= value;

+= 这样的赋值运算符将算数运算符和赋值运算符组合在了一起。

= *= %= >>>= ^=
+= /= <<= &= ` =`
-= ~/= >>=

下表解释了符合运算符的原理:

场景 复合运算 等效表达式
假设有运算符 op a *op*= b a = a *op* b
示例: a += b a = a + b

下面的例子展示了如何使用赋值以及复合赋值运算符:

  1. var a = 2; // Assign using =
  2. a *= 3; // Assign and multiply: a = a * 3
  3. assert(a == 6);

逻辑运算符

使用逻辑运算符你可以反转或组合布尔表达式。

运算符 描述
!*表达式* 对表达式结果取反(即将 true 变为 false,false 变为 true)
` ` 逻辑或
&& 逻辑与

下面是使用逻辑表达式的示例:

  1. if (!done && (col == 0 || col == 3)) {
  2. // ...Do something...
  3. }

按位和移位运算符

在 Dart 中,二进制位运算符可以操作二进制的某一位,但仅适用于整数。

运算符 描述
& 按位与
` ` 按位或
^ 按位异或
~*表达式* 按位取反(即将 “0” 变为 “1”,“1” 变为 “0”)
<< 位左移
>> 位右移
>>> 无符号右移

下面是使用按位和移位运算符的示例:

  1. final value = 0x22;
  2. final bitmask = 0x0f;
  3. assert((value & bitmask) == 0x02); // AND
  4. assert((value & ~bitmask) == 0x20); // AND NOT
  5. assert((value | bitmask) == 0x2f); // OR
  6. assert((value ^ bitmask) == 0x2d); // XOR
  7. assert((value << 4) == 0x220); // Shift left
  8. assert((value >> 4) == 0x02); // Shift right
  9. assert((value >>> 4) == 0x02); // Unsigned shift right
  10. assert((-value >> 4) == -0x03); // Shift right
  11. assert((-value >>> 4) > 0); // Unsigned shift right

版本提示:

>>> 操作符在 2.14 以上的 Dart 版本 中可用。

条件表达式

Dart 有两个特殊的运算符可以用来替代 if-else 语句:

*条件* ? *表达式 1* : *表达式 2*
如果条件为 true,执行表达式 1并返回执行结果,否则执行表达式 2 并返回执行结果。

*表达式 1* ?? *表达式 2*
如果表达式 1 为非 null 则返回其值,否则执行表达式 2 并返回其值。

根据布尔表达式确定赋值时,请考虑使用 ?:

  1. var visibility = isPublic ? 'public' : 'private';

如果赋值是根据判定是否为 null 则考虑使用 ??

  1. String playerName(String? name) => name ?? 'Guest';

上述示例还可以写成至少下面两种不同的形式,只是不够简洁:

  1. // Slightly longer version uses ?: operator.
  2. String playerName(String? name) => name != null ? name : 'Guest';
  3. // Very long version uses if-else statement.
  4. String playerName(String? name) {
  5. if (name != null) {
  6. return name;
  7. } else {
  8. return 'Guest';
  9. }
  10. }

级联运算符

级联运算符 (.., ?..) 可以让你在同一个对象上连续调用多个对象的变量或方法。

比如下面的代码:

  1. var paint = Paint()
  2. ..color = Colors.black
  3. ..strokeCap = StrokeCap.round
  4. ..strokeWidth = 5.0;

The constructor, Paint(), returns a Paint object. The code that follows the cascade notation operates on this object, ignoring any values that might be returned.

The previous example is equivalent to this code:

  1. var paint = Paint();
  2. paint.color = Colors.black;
  3. paint.strokeCap = StrokeCap.round;
  4. paint.strokeWidth = 5.0;

If the object that the cascade operates on can be null, then use a null-shorting cascade (?..) for the first operation. Starting with ?.. guarantees that none of the cascade operations are attempted on that null object.

  1. querySelector('#confirm') // Get an object.
  2. ?..text = 'Confirm' // Use its members.
  3. ..classes.add('important')
  4. ..onClick.listen((e) => window.alert('Confirmed!'));

版本提示:

?.. 运行在 2.12 和以上的 版本 中可用。

上面的代码相当于:

  1. var button = querySelector('#confirm');
  2. button?.text = 'Confirm';
  3. button?.classes.add('important');
  4. button?.onClick.listen((e) => window.alert('Confirmed!'));

级联运算符可以嵌套,例如:

  1. final addressBook = (AddressBookBuilder()
  2. ..name = 'jenny'
  3. ..email = 'jenny@example.com'
  4. ..phone = (PhoneNumberBuilder()
  5. ..number = '415-555-0100'
  6. ..label = 'home')
  7. .build())
  8. .build();

在返回对象的函数中谨慎使用级联操作符。例如,下面的代码是错误的:

  1. var sb = StringBuffer();
  2. sb.write('foo')
  3. ..write('bar'); // Error: method 'write' isn't defined for 'void'.

上述代码中的 sb.write() 方法返回的是 void,返回值为 void 的方法则不能使用级联运算符。

备忘:

严格来说 .. 级联操作并非一个运算符而是 Dart 的特殊语法。

其他运算符

大多数其它的运算符,已经在其它的示例中使用过:

运算符 名字 描述
() 使用方法 代表调用一个方法
[] 访问 List 访问 List 中特定位置的元素
?[] 判空访问 List 左侧调用者不为空时,访问 List 中特定位置的元素
. 访问成员 成员访问符
?. 条件访问成员 与上述成员访问符类似,但是左边的操作对象不能为 null,例如 foo?.bar,如果 foo 为 null 则返回 null ,否则返回 bar

更多关于 ., ?... 运算符介绍,请参考.

流程控制语句

你可以使用下面的语句来控制 Dart 代码的执行流程:

  • ifelse
  • for 循环
  • whiledo-while 循环
  • breakcontinue
  • switchcase
  • assert

使用 try-catchthrow 也能影响控制流,详情参考异常部分。

If 和 Else

Dart 支持 if - else 语句,其中 else 是可选的,比如下面的例子。你也可以参考条件表达式

  1. if (isRaining()) {
  2. you.bringRainCoat();
  3. } else if (isSnowing()) {
  4. you.wearJacket();
  5. } else {
  6. car.putTopDown();
  7. }

不同于 JavaScript,Dart 的 if 语句中的条件必须是布尔值而不能为其它类型。详情请查阅布尔值

For 循环

你可以使用标准的 for 循环进行迭代。例如:

  1. var message = StringBuffer('Dart is fun');
  2. for (var i = 0; i < 5; i++) {
  3. message.write('!');
  4. }

在 Dart 语言中,for 循环中的闭包会自动捕获循环的 索引值 以避免 JavaScript 中一些常见的陷阱。假设有如下代码:

  1. var callbacks = [];
  2. for (var i = 0; i < 2; i++) {
  3. callbacks.add(() => print(i));
  4. }
  5. callbacks.forEach((c) => c());

上述代码执行后会输出 01,但是如果在 JavaScript 中执行同样的代码则会输出两个 2

如果要遍历的对象是一个可迭代对象(例如 List 或 Set),并且你不需要知道当前的遍历索引,则可以使用 for-in 方法进行 遍历

  1. for (final candidate in candidates) {
  2. candidate.interview();
  3. }

小提示:

若想练习使用 for-in,请参考 遍历集合 codelab

可迭代对象同时可以使用 forEach() 方法作为另一种选择:

  1. var collection = [1, 2, 3];
  2. collection.forEach(print); // 1 2 3

While 和 Do-While

while 循环会在执行循环体前先判断条件:

  1. while (!isDone()) {
  2. doSomething();
  3. }

do-while 循环则会 先执行一遍循环体 再判断条件:

  1. do {
  2. printLine();
  3. } while (!atEndOfPage());

Break 和 Continue

使用 break 可以中断循环:

  1. while (true) {
  2. if (shutDownRequested()) break;
  3. processIncomingRequests();
  4. }

使用 continue 可以跳过本次循环直接进入下一次循环:

  1. for (int i = 0; i < candidates.length; i++) {
  2. var candidate = candidates[i];
  3. if (candidate.yearsExperience < 5) {
  4. continue;
  5. }
  6. candidate.interview();
  7. }

如果你正在使用诸如 List 或 Set 之类的 Iterable 对象,你可以用以下方式重写上述例子:

  1. candidates
  2. .where((c) => c.yearsExperience >= 5)
  3. .forEach((c) => c.interview());

Switch 和 Case

Switch 语句在 Dart 中使用 == 来比较整数、字符串或编译时常量,比较的两个对象必须是同一个类型且不能是子类并且没有重写 == 操作符。 枚举类型非常适合在 Switch 语句中使用。

备忘:

Dart 中的 Switch 语句仅适用于有限的情况,比如使用解释器和扫描器的场景。

每一个非空的 case 子句都必须有一个 break 语句,也可以通过 continuethrow 或者 return 来结束非空 case 语句。

不匹配任何 case 语句的情况下,会执行 default 子句中的代码:

  1. var command = 'OPEN';
  2. switch (command) {
  3. case 'CLOSED':
  4. executeClosed();
  5. break;
  6. case 'PENDING':
  7. executePending();
  8. break;
  9. case 'APPROVED':
  10. executeApproved();
  11. break;
  12. case 'DENIED':
  13. executeDenied();
  14. break;
  15. case 'OPEN':
  16. executeOpen();
  17. break;
  18. default:
  19. executeUnknown();
  20. }

下面的例子忽略了 case 子句的 break 语句,因此会产生错误:

  1. var command = 'OPEN';
  2. switch (command) {
  3. case 'OPEN':
  4. executeOpen();
  5. // ERROR: Missing break
  6. case 'CLOSED':
  7. executeClosed();
  8. break;
  9. }

但是,Dart 支持空的 case 语句,允许其以 fall-through 的形式执行。

  1. var command = 'CLOSED';
  2. switch (command) {
  3. case 'CLOSED': // Empty case falls through.
  4. case 'NOW_CLOSED':
  5. // Runs for both CLOSED and NOW_CLOSED.
  6. executeNowClosed();
  7. break;
  8. }

在非空 case 语句中想要实现 fall-through 的形式,可以使用 continue 语句配合 label 的方式实现:

  1. var command = 'CLOSED';
  2. switch (command) {
  3. case 'CLOSED':
  4. executeClosed();
  5. continue nowClosed;
  6. // Continues executing at the nowClosed label.
  7. nowClosed:
  8. case 'NOW_CLOSED':
  9. // Runs for both CLOSED and NOW_CLOSED.
  10. executeNowClosed();
  11. break;
  12. }

每个 case 子句都可以有局部变量且仅在该 case 语句内可见。

断言

在开发过程中,可以在条件表达式为 false 时使用 — assert(*条件*, *可选信息*); — 语句来打断代码的执行。你可以在本文中找到大量使用 assert 的例子。下面是相关示例:

  1. // Make sure the variable has a non-null value.
  2. assert(text != null);
  3. // Make sure the value is less than 100.
  4. assert(number < 100);
  5. // Make sure this is an https URL.
  6. assert(urlString.startsWith('https'));

assert 的第二个参数可以为其添加一个字符串消息。

  1. assert(urlString.startsWith('https'),
  2. 'URL ($urlString) should start with "https".');

assert 的第一个参数可以是值为布尔值的任何表达式。如果表达式的值为 true,则断言成功,继续执行。如果表达式的值为 false,则断言失败,抛出一个 AssertionError 异常。

如何判断 assert 是否生效?assert 是否生效依赖开发工具和使用的框架:

  • Flutter 在调试模式时生效。
  • 一些开发工具比如 dartdevc 通常情况下是默认生效的。
  • 其他一些工具,比如 dart run以及 dart2js 通过在运行 Dart 程序时添加命令行参数 --enable-asserts 使 assert 生效。

在生产环境代码中,断言会被忽略,与此同时传入 assert 的参数不被判断。

异常

Dart 代码可以抛出和捕获异常。异常表示一些未知的错误情况,如果异常没有捕获则会被抛出从而导致抛出异常的代码终止执行。

与 Java 不同的是,Dart 的所有异常都是非必检异常,方法不必声明会抛出哪些异常,并且你也不必捕获任何异常。

Dart 提供了 ExceptionError 两种类型的异常以及它们一系列的子类,你也可以定义自己的异常类型。但是在 Dart 中可以将任何非 null 对象作为异常抛出而不局限于 Exception 或 Error 类型。

抛出异常

下面是关于抛出或者 引发 异常的示例:

  1. throw FormatException('Expected at least 1 section');

你也可以抛出任意的对象:

  1. throw 'Out of llamas!';

备忘:

优秀的代码通常会抛出 ErrorException 类型的异常。

因为抛出异常是一个表达式,所以可以在 => 语句中使用,也可以在其他使用表达式的地方抛出异常:

  1. void distanceTo(Point other) => throw UnimplementedError();

捕获异常

捕获异常可以避免异常继续传递(重新抛出异常除外)。捕获一个异常可以给你处理它的机会:

  1. try {
  2. breedMoreLlamas();
  3. } on OutOfLlamasException {
  4. buyMoreLlamas();
  5. }

对于可以抛出多种异常类型的代码,也可以指定多个 catch 语句,每个语句分别对应一个异常类型,如果 catch 语句没有指定异常类型则表示可以捕获任意异常类型:

  1. try {
  2. breedMoreLlamas();
  3. } on OutOfLlamasException {
  4. // A specific exception
  5. buyMoreLlamas();
  6. } on Exception catch (e) {
  7. // Anything else that is an exception
  8. print('Unknown exception: $e');
  9. } catch (e) {
  10. // No specified type, handles all
  11. print('Something really unknown: $e');
  12. }

如上述代码所示可以使用 oncatch 来捕获异常,使用 on 来指定异常类型,使用 catch 来捕获异常对象,两者可同时使用。

你可以为 catch 方法指定两个参数,第一个参数为抛出的异常对象,第二个参数为栈信息 StackTrace 对象:

  1. try {
  2. // ···
  3. } on Exception catch (e) {
  4. print('Exception details:\n $e');
  5. } catch (e, s) {
  6. print('Exception details:\n $e');
  7. print('Stack trace:\n $s');
  8. }

关键字 rethrow 可以将捕获的异常再次抛出:

  1. void misbehave() {
  2. try {
  3. dynamic foo = true;
  4. print(foo++); // Runtime error
  5. } catch (e) {
  6. print('misbehave() partially handled ${e.runtimeType}.');
  7. rethrow; // Allow callers to see the exception.
  8. }
  9. }
  10. void main() {
  11. try {
  12. misbehave();
  13. } catch (e) {
  14. print('main() finished handling ${e.runtimeType}.');
  15. }
  16. }

Finally

无论是否抛出异常,finally 语句始终执行,如果没有指定 catch 语句来捕获异常,则异常会在执行完 finally 语句后抛出:

  1. try {
  2. breedMoreLlamas();
  3. } finally {
  4. // Always clean up, even if an exception is thrown.
  5. cleanLlamaStalls();
  6. }

finally 语句会在任何匹配的 catch 语句后执行:

  1. try {
  2. breedMoreLlamas();
  3. } catch (e) {
  4. print('Error: $e'); // Handle the exception first.
  5. } finally {
  6. cleanLlamaStalls(); // Then clean up.
  7. }

你可以阅读 Dart 核心库概览的 异常 章节获取更多相关信息。

Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null 以外的所有的类都继承自 Object 类。 基于 mixin 的继承 意味着尽管每个类(top class Object? 除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 扩展方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。

使用类的成员

对象的 成员 由函数和数据(即 方法实例变量)组成。方法的 调用 要通过对象来完成,这种方式可以访问对象的函数和数据。

使用(.)来访问对象的实例变量或方法:

  1. var p = Point(2, 2);
  2. // Get the value of y.
  3. assert(p.y == 2);
  4. // Invoke distanceTo() on p.
  5. double distance = p.distanceTo(Point(4, 4));

使用 ?. 代替 . 可以避免因为左边表达式为 null 而导致的问题:

  1. // If p is non-null, set a variable equal to its y value.
  2. var a = p?.y;

使用构造函数

可以使用 构造函数 来创建一个对象。构造函数的命名方式可以为 *类名**类名* . *标识符*的形式。例如下述代码分别使用 Point()Point.fromJson() 两种构造器创建了 Point 对象:

  1. var p1 = Point(2, 2);
  2. var p2 = Point.fromJson({'x': 1, 'y': 2});

以下代码具有相同的效果,但是构造函数名前面的的 new 关键字是可选的:

  1. var p1 = new Point(2, 2);
  2. var p2 = new Point.fromJson({'x': 1, 'y': 2});

一些类提供了常量构造函数。使用常量构造函数,在构造函数名之前加 const 关键字,来创建编译时常量时:

  1. var p = const ImmutablePoint(2, 2);

两个使用相同构造函数相同参数值构造的编译时常量是同一个对象:

  1. var a = const ImmutablePoint(1, 1);
  2. var b = const ImmutablePoint(1, 1);
  3. assert(identical(a, b)); // They are the same instance!

常量上下文 场景中,你可以省略掉构造函数或字面量前的 const 关键字。例如下面的例子中我们创建了一个常量 Map:

  1. // Lots of const keywords here.
  2. const pointAndLine = const {
  3. 'point': const [const ImmutablePoint(0, 0)],
  4. 'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
  5. };

根据上下文,你可以只保留第一个 const 关键字,其余的全部省略:

  1. // Only one const, which establishes the constant context.
  2. const pointAndLine = {
  3. 'point': [ImmutablePoint(0, 0)],
  4. 'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
  5. };

但是如果无法根据上下文判断是否可以省略 const,则不能省略掉 const 关键字,否则将会创建一个 非常量对象 例如:

  1. var a = const ImmutablePoint(1, 1); // Creates a constant
  2. var b = ImmutablePoint(1, 1); // Does NOT create a constant
  3. assert(!identical(a, b)); // NOT the same instance!

获取对象的类型

可以使用 Object 对象的 runtimeType 属性在运行时获取一个对象的类型,该对象类型是 Type 的实例。

  1. print('The type of a is ${a.runtimeType}');

Use a type test operator rather than runtimeType to test an object’s type. In production environments, the test object is Type is more stable than the test object.runtimeType == Type.

到目前为止,我们已经解了如何 使用 类。本节的其余部分将向你介绍如何 实现 一个类。

实例变量

下面是声明实例变量的示例:

  1. class Point {
  2. double? x; // Declare instance variable x, initially null.
  3. double? y; // Declare y, initially null.
  4. double z = 0; // Declare z, initially 0.
  5. }

所有未初始化的实例变量其值均为 null

所有实例变量均会隐式地声明一个 Getter 方法。非终值的实例变量和 late final 声明但未声明初始化的实例变量还会隐式地声明一个 Setter 方法。你可以查阅 Getter 和 Setter 获取更多相关信息。

  1. class Point {
  2. double? x; // Declare instance variable x, initially null.
  3. double? y; // Declare y, initially null.
  4. }
  5. void main() {
  6. var point = Point();
  7. point.x = 4; // Use the setter method for x.
  8. assert(point.x == 4); // Use the getter method for x.
  9. assert(point.y == null); // Values default to null.
  10. }

Instance variables can be final, in which case they must be set exactly once. Initialize final, non-late instance variables at declaration, using a constructor parameter, or using a constructor’s initializer list:

  1. class ProfileMark {
  2. final String name;
  3. final DateTime start = DateTime.now();
  4. ProfileMark(this.name);
  5. ProfileMark.unnamed() : name = '';
  6. }

If you need to assign the value of a final instance variable after the constructor body starts, you can use one of the following:

构造函数

声明一个与类名一样的函数即可声明一个构造函数(对于命名式构造函数 还可以添加额外的标识符)。大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:

  1. class Point {
  2. double x = 0;
  3. double y = 0;
  4. Point(double x, double y) {
  5. // There's a better way to do this, stay tuned.
  6. this.x = x;
  7. this.y = y;
  8. }
  9. }

使用 this 关键字引用当前实例。

备忘:

当且仅当命名冲突时使用 this 关键字才有意义,否则 Dart 会忽略 this 关键字。

对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤:

  1. class Point {
  2. double x = 0;
  3. double y = 0;
  4. // Syntactic sugar for setting x and y
  5. // before the constructor body runs.
  6. Point(this.x, this.y);
  7. }

默认构造函数

如果你没有声明构造函数,那么 Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法。

构造函数不被继承

子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。

命名式构造函数

可以为一个类声明多个命名式构造函数来表达更明确的意图:

  1. const double xOrigin = 0;
  2. const double yOrigin = 0;
  3. class Point {
  4. double x = 0;
  5. double y = 0;
  6. Point(this.x, this.y);
  7. // Named constructor
  8. Point.origin()
  9. : x = xOrigin,
  10. y = yOrigin;
  11. }

记住构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数,如果你想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。

调用父类非默认构造函数

默认情况下,子类的构造函数会调用父类的匿名无参数构造方法,并且该调用会在子类构造函数的函数体代码执行前,如果子类构造函数还有一个 初始化列表,那么该初始化列表会在调用父类的该构造函数之前被执行,总的来说,这三者的调用顺序如下:

  1. 初始化列表
  2. 父类的无参数构造函数
  3. 当前类的构造函数

如果父类没有匿名无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用(:)指定。

因为参数会在子类构造函数被执行前传递给父类的构造函数,因此该参数也可以是一个表达式,比如一个函数:

  1. class Employee extends Person {
  2. Employee() : super.fromJson(fetchDefaultData());
  3. // ···
  4. }

请注意:

传递给父类构造函数的参数不能使用 this 关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员都不能被访问,但是类成员可以。

初始化列表

除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。每个实例变量之间使用逗号分隔。

  1. // Initializer list sets instance variables before
  2. // the constructor body runs.
  3. Point.fromJson(Map<String, double> json)
  4. : x = json['x']!,
  5. y = json['y']! {
  6. print('In Point.fromJson(): ($x, $y)');
  7. }

请注意:

初始化列表表达式 = 右边的语句不能使用 this 关键字。

在开发模式下,你可以在初始化列表中使用 assert 来验证输入数据:

  1. Point.withAssert(this.x, this.y) : assert(x >= 0) {
  2. print('In Point.withAssert(): ($x, $y)');
  3. }

重定向构造函数

有时候类中的构造函数仅用于调用类中其它的构造函数,此时该构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数 (使用 this 而非类名):

  1. class Point {
  2. double x, y;
  3. // The main constructor for this class.
  4. Point(this.x, this.y);
  5. // Delegates to the main constructor.
  6. Point.alongXAxis(double x) : this(x, 0);
  7. }

常量构造函数

如果类生成的对象都是不变的,可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能。

  1. class ImmutablePoint {
  2. static const ImmutablePoint origin = ImmutablePoint(0, 0);
  3. final double x, y;
  4. const ImmutablePoint(this.x, this.y);
  5. }

常量构造函数创建的实例并不总是常量,具体可以参考使用构造函数章节。

工厂构造函数

使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。

小提示:

另一种处理懒加载变量的方式是 使用 late final(谨慎使用)

在如下的示例中, Logger 的工厂构造函数从缓存中返回对象,和 Logger.fromJson 工厂构造函数从 JSON 对象中初始化一个最终变量。

  1. class Logger {
  2. final String name;
  3. bool mute = false;
  4. // _cache is library-private, thanks to
  5. // the _ in front of its name.
  6. static final Map<String, Logger> _cache =
  7. <String, Logger>{};
  8. factory Logger(String name) {
  9. return _cache.putIfAbsent(
  10. name, () => Logger._internal(name));
  11. }
  12. factory Logger.fromJson(Map<String, Object> json) {
  13. return Logger(json['name'].toString());
  14. }
  15. Logger._internal(this.name);
  16. void log(String msg) {
  17. if (!mute) print(msg);
  18. }
  19. }

备忘:

在工厂构造函数中无法访问 this

工厂构造函数的调用方式与其他构造函数一样:

  1. var logger = Logger('UI');
  2. logger.log('Button clicked');
  3. var logMap = {'name': 'UI'};
  4. var loggerJson = Logger.fromJson(logMap);

方法

方法是为对象提供行为的函数。

实例方法

对象的实例方法可以访问实例变量和 this。下面的 distanceTo() 方法就是一个实例方法的例子:

  1. import 'dart:math';
  2. class Point {
  3. double x = 0;
  4. double y = 0;
  5. Point(this.x, this.y);
  6. double distanceTo(Point other) {
  7. var dx = x - other.x;
  8. var dy = y - other.y;
  9. return sqrt(dx * dx + dy * dy);
  10. }
  11. }

操作符

运算符是有着特殊名称的实例方法。 Dart 允许您使用以下名称定义运算符:

< + ` ` >>>
> / ^ []
<= ~/ & []=
>= * << ~
% >> ==

备忘:

你可能注意到有一些 操作符 没有出现在列表中,例如 !=。因为它们仅仅是语法糖。表达式 e1 != e2 仅仅是 !(e1 == e2) 的一个语法糖。

为了表示重写操作符,我们使用 operator 标识来进行标记。下面是重写 +- 操作符的例子

  1. class Vector {
  2. final int x, y;
  3. Vector(this.x, this.y);
  4. Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  5. Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
  6. // Operator == and hashCode not shown.
  7. // ···
  8. }
  9. void main() {
  10. final v = Vector(2, 3);
  11. final w = Vector(2, 2);
  12. assert(v + w == Vector(4, 5));
  13. assert(v - w == Vector(0, 1));
  14. }

Getter 和 Setter

Getter 和 Setter 是一对用来读写对象属性的特殊方法,上面说过实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,你可以使用 getset 关键字为额外的属性添加 Getter 和 Setter 方法:

  1. class Rectangle {
  2. double left, top, width, height;
  3. Rectangle(this.left, this.top, this.width, this.height);
  4. // Define two calculated properties: right and bottom.
  5. double get right => left + width;
  6. set right(double value) => left = value - width;
  7. double get bottom => top + height;
  8. set bottom(double value) => top = value - height;
  9. }
  10. void main() {
  11. var rect = Rectangle(3, 4, 20, 15);
  12. assert(rect.left == 3);
  13. rect.right = 12;
  14. assert(rect.left == -8);
  15. }

使用 Getter 和 Setter 的好处是,你可以先使用你的实例变量,过一段时间过再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑。

备忘:

像自增(++)这样的操作符不管是否定义了 Getter 方法都会正确地执行。为了避免一些不必要的异常情况,运算符只会调用 Getter 一次,然后将其值存储在一个临时变量中。

抽象方法

实例方法、Getter 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于 抽象类中。

直接使用分号(;)替代方法体即可声明一个抽象方法:

  1. abstract class Doer {
  2. // Define instance variables and methods...
  3. void doSomething(); // Define an abstract method.
  4. }
  5. class EffectiveDoer extends Doer {
  6. void doSomething() {
  7. // Provide an implementation, so the method is not abstract here...
  8. }
  9. }

抽象类

使用关键字 abstract 标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时可被实例化,可以为其定义 工厂构造函数

抽象类常常会包含 抽象方法。下面是一个声明具有抽象方法的抽象类示例:

  1. // This class is declared abstract and thus
  2. // can't be instantiated.
  3. abstract class AbstractContainer {
  4. // Define constructors, fields, methods...
  5. void updateChildren(); // Abstract method.
  6. }

隐式接口

每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。

一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:

  1. // A person. The implicit interface contains greet().
  2. class Person {
  3. // In the interface, but visible only in this library.
  4. final String _name;
  5. // Not in the interface, since this is a constructor.
  6. Person(this._name);
  7. // In the interface.
  8. String greet(String who) => 'Hello, $who. I am $_name.';
  9. }
  10. // An implementation of the Person interface.
  11. class Impostor implements Person {
  12. String get _name => '';
  13. String greet(String who) => 'Hi $who. Do you know who I am?';
  14. }
  15. String greetBob(Person person) => person.greet('Bob');
  16. void main() {
  17. print(greetBob(Person('Kathy')));
  18. print(greetBob(Impostor()));
  19. }

如果需要实现多个类接口,可以使用逗号分割每个接口类:

  1. class Point implements Comparable, Location {...}

扩展一个类

使用 extends 关键字来创建一个子类,并可使用 super 关键字引用一个父类:

  1. class Television {
  2. void turnOn() {
  3. _illuminateDisplay();
  4. _activateIrSensor();
  5. }
  6. // ···
  7. }
  8. class SmartTelevision extends Television {
  9. void turnOn() {
  10. super.turnOn();
  11. _bootNetworkInterface();
  12. _initializeMemory();
  13. _upgradeApps();
  14. }
  15. // ···
  16. }

想了解其他 extends 的用法,

重写类成员

子类可以重写父类的实例方法(包括 操作符)、 Getter 以及 Setter 方法。你可以使用 @override 注解来表示你重写了一个成员:

  1. class Television {
  2. // ···
  3. set contrast(int value) {...}
  4. }
  5. class SmartTelevision extends Television {
  6. @override
  7. set contrast(num value) {...}
  8. // ···
  9. }

An overriding method declaration must match the method (or methods) that it overrides in several ways:

  • The return type must be the same type as (or a subtype of) the overridden method’s return type.
  • Argument types must be the same type as (or a supertype of) the overridden method’s argument types. In the preceding example, the contrast setter of SmartTelevision changes the argument type from int to a supertype, num.
  • If the overridden method accepts n positional parameters, then the overriding method must also accept n positional parameters.
  • A generic method can’t override a non-generic one, and a non-generic method can’t override a generic one.

你可以使用 covariant 关键字 来缩小代码中那些符合 类型安全 的方法参数或实例变量的类型。

请注意:

如果重写 == 操作符,必须同时重写对象 hashCode 的 Getter 方法。你可以查阅 实现映射键 获取更多关于重写的 ==hashCode 的例子。

noSuchMethod 方法

如果调用了对象上不存在的方法或实例变量将会触发 noSuchMethod 方法,你可以重写 noSuchMethod 方法来追踪和记录这一行为:

  1. class A {
  2. // Unless you override noSuchMethod, using a
  3. // non-existent member results in a NoSuchMethodError.
  4. @override
  5. void noSuchMethod(Invocation invocation) {
  6. print('You tried to use a non-existent member: '
  7. '${invocation.memberName}');
  8. }
  9. }

只有下面其中一个条件成立时,你才能调用一个未实现的方法:

  • 接收方是静态的 dynamic 类型。
  • 接收方具有静态类型,定义了未实现的方法(抽象亦可),并且接收方的动态类型实现了 noSuchMethod 方法且具体的实现与 Object 中的不同。

你可以查阅 noSuchMethod 转发规范 获取更多相关信息。

扩展方法

扩展方法是向现有库添加功能的一种方式。你可能已经在不知道它是扩展方法的情况下使用了它。例如,当您在 IDE 中使用代码完成功能时,它建议将扩展方法与常规方法一起使用。

这里是一个在 String 中使用扩展方法的样例,我们取名为 parseInt(),它在 string_apis.dart 中定义:

  1. import 'string_apis.dart';
  2. ...
  3. print('42'.padLeft(5)); // Use a String method.
  4. print('42'.parseInt()); // Use an extension method.

有关使用以及实现扩展方法的详细信息,请参阅 扩展方法页面

枚举类型

枚举类型是一种特殊的类型,也称为 enumerationsenums,用于定义一些固定数量的常量值。

Using enums

使用枚举

使用关键字 enum 来定义枚举类型:

  1. enum Color { red, green, blue }

你可以在声明枚举类型时使用 尾随逗号

每一个枚举值都有一个名为 index 成员变量的 Getter 方法,该方法将会返回以 0 为基准索引的位置值。例如,第一个枚举值的索引是 0 ,第二个枚举值的索引是 1。以此类推。

  1. assert(Color.red.index == 0);
  2. assert(Color.green.index == 1);
  3. assert(Color.blue.index == 2);

想要获得全部的枚举值,使用枚举类的 values 方法获取包含它们的列表:

  1. List<Color> colors = Color.values;
  2. assert(colors[2] == Color.blue);

你可以在 Switch 语句中使用枚举,但是需要注意的是必须处理枚举值的每一种情况,即每一个枚举值都必须成为一个 case 子句,不然会出现警告:

  1. var aColor = Color.blue;
  2. switch (aColor) {
  3. case Color.red:
  4. print('Red as roses!');
  5. break;
  6. case Color.green:
  7. print('Green as grass!');
  8. break;
  9. default: // Without this, you see a WARNING.
  10. print(aColor); // 'Color.blue'
  11. }

枚举类型有如下两个限制:

  • 枚举不能成为子类,也不可以 mix in,你也不可以实现一个枚举。
  • 不能显式地实例化一个枚举类。

你可以查阅 Dart 编程语言规范 获取更多相关信息。

使用 Mixin 为类添加功能

Mixin 是一种在多重继承中复用某个类中代码的方法模式。

使用 with 关键字并在其后跟上 Mixin 类的名字来使用 Mixin 模式:

  1. class Musician extends Performer with Musical {
  2. // ···
  3. }
  4. class Maestro extends Person
  5. with Musical, Aggressive, Demented {
  6. Maestro(String maestroName) {
  7. name = maestroName;
  8. canConduct = true;
  9. }
  10. }

想要实现一个 Mixin,请创建一个继承自 Object 且未声明构造函数的类。除非你想让该类与普通的类一样可以被正常地使用,否则请使用关键字 mixin 替代 class。例如:

  1. mixin Musical {
  2. bool canPlayPiano = false;
  3. bool canCompose = false;
  4. bool canConduct = false;
  5. void entertainMe() {
  6. if (canPlayPiano) {
  7. print('Playing piano');
  8. } else if (canConduct) {
  9. print('Waving hands');
  10. } else {
  11. print('Humming to self');
  12. }
  13. }
  14. }

可以使用关键字 on 来指定哪些类可以使用该 Mixin 类,比如有 Mixin 类 A,但是 A 只能被 B 类使用,则可以这样定义 A:

  1. class Musician {
  2. // ...
  3. }
  4. mixin MusicalPerformer on Musician {
  5. // ...
  6. }
  7. class SingerDancer extends Musician with MusicalPerformer {
  8. // ...
  9. }

In the preceding code, only classes that extend or implement the Musician class can use the mixin MusicalPerformer. Because SingerDancer extends Musician, SingerDancer can mix in MusicalPerformer.

版本提示:

mixin 关键字在 Dart 2.1 中才被引用支持。早期版本中的代码通常使用 abstract class 代替。你可以查阅 Dart SDK 变更日志2.1 mixin 规范 获取更多有关 Mixin 在 2.1 中的变更信息。

类变量和方法

使用关键字 static 可以声明类变量或类方法。

静态变量

静态变量(即类变量)常用于声明类范围内所属的状态变量和常量:

  1. class Queue {
  2. static const initialCapacity = 16;
  3. // ···
  4. }
  5. void main() {
  6. assert(Queue.initialCapacity == 16);
  7. }

静态变量在其首次被使用的时候才被初始化。

备忘:

本文代码准守 风格推荐指南 中的命名规则,使用 驼峰式大小写 来命名常量。

静态方法

静态方法(即类方法)不能对实例进行操作,因此不能使用 this。但是他们可以访问静态变量。如下面的例子所示,你可以在一个类上直接调用静态方法:

  1. import 'dart:math';
  2. class Point {
  3. double x, y;
  4. Point(this.x, this.y);
  5. static double distanceBetween(Point a, Point b) {
  6. var dx = a.x - b.x;
  7. var dy = a.y - b.y;
  8. return sqrt(dx * dx + dy * dy);
  9. }
  10. }
  11. void main() {
  12. var a = Point(2, 2);
  13. var b = Point(4, 4);
  14. var distance = Point.distanceBetween(a, b);
  15. assert(2.8 < distance && distance < 2.9);
  16. print(distance);
  17. }

备忘:

对于一些通用或常用的静态方法,应该将其定义为顶级函数而非静态方法。

可以将静态方法作为编译时常量。例如,你可以将静态方法作为一个参数传递给一个常量构造函数。

泛型

如果你查看数组的 API 文档,你会发现数组 List 的实际类型为 List<E>。 <…> 符号表示数组是一个 泛型(或 参数化类型通常 使用一个字母来代表类型参数,比如 E、T、S、K 和 V 等等。

为什么使用泛型?

泛型常用于需要要求类型安全的情况,但是它也会对代码运行有好处:

  • 适当地指定泛型可以更好地帮助代码生成。
  • 使用泛型可以减少代码重复。

比如你想声明一个只能包含 String 类型的数组,你可以将该数组声明为 List<String>(读作“字符串类型的 list”),这样的话就可以很容易避免因为在该数组放入非 String 类变量而导致的诸多问题,同时编译器以及其他阅读代码的人都可以很容易地发现并定位问题:

  1. var names = <String>[];
  2. names.addAll(['Seth', 'Kathy', 'Lars']);
  3. names.add(42); // Error

另一个使用泛型的原因是可以减少重复代码。泛型可以让你在多个不同类型实现之间共享同一个接口声明,比如下面的例子中声明了一个类用于缓存对象的接口:

  1. abstract class ObjectCache {
  2. Object getByKey(String key);
  3. void setByKey(String key, Object value);
  4. }

不久后你可能又会想专门为 String 类对象做一个缓存,于是又有了专门为 String 做缓存的类:

  1. abstract class StringCache {
  2. String getByKey(String key);
  3. void setByKey(String key, String value);
  4. }

如果过段时间你又想为数字类型也创建一个类,那么就会有很多诸如此类的代码……

这时候可以考虑使用泛型来声明一个类,让不同类型的缓存实现该类做出不同的具体实现即可:

  1. abstract class Cache<T> {
  2. T getByKey(String key);
  3. void setByKey(String key, T value);
  4. }

在上述代码中,T 是一个替代类型。其相当于类型占位符,在开发者调用该接口的时候会指定具体类型。

使用集合字面量

List、Set 以及 Map 字面量也可以是参数化的。定义参数化的 List 只需在中括号前添加 <*type*>;定义参数化的 Map 只需要在大括号前添加 <*keyType*, *valueType*>

  1. var names = <String>['Seth', 'Kathy', 'Lars'];
  2. var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
  3. var pages = <String, String>{
  4. 'index.html': 'Homepage',
  5. 'robots.txt': 'Hints for web robots',
  6. 'humans.txt': 'We are people, not machines'
  7. };

使用类型参数化的构造函数

在调用构造方法时也可以使用泛型,只需在类名后用尖括号(<...>)将一个或多个类型包裹即可:

  1. var nameSet = Set<String>.from(names);

下面代码创建了一个键为 Int 类型,值为 View 类型的 Map 对象:

  1. var views = Map<int, View>();

泛型集合以及它们所包含的类型

Dart的泛型类型是 固化的,这意味着即便在运行时也会保持类型信息:

  1. var names = <String>[];
  2. names.addAll(['Seth', 'Kathy', 'Lars']);
  3. print(names is List<String>); // true

备忘:

与 Java 不同的是,Java 中的泛型是类型 擦除 的,这意味着泛型类型会在运行时被移除。在 Java 中你可以判断对象是否为 List 但不可以判断对象是否为 List<String>

限制参数化类型

有时使用泛型的时候,你可能会想限制可作为参数的泛型范围,也就是参数必须是指定类型的子类,这时候可以使用 extends 关键字。

一种常见的非空类型处理方式,是将子类限制继承 Object (而不是默认的 Object?)。

  1. class Foo<T extends Object> {
  2. // Any type provided to Foo for T must be non-nullable.
  3. }

You can use extends with other types besides Object. Here’s an example of extending SomeBaseClass, so that members of SomeBaseClass can be called on objects of type T:

  1. class Foo<T extends SomeBaseClass> {
  2. // Implementation goes here...
  3. String toString() => "Instance of 'Foo<$T>'";
  4. }
  5. class Extender extends SomeBaseClass {...}

这时候就可以使用 SomeBaseClass 或者它的子类来作为泛型参数:

  1. var someBaseClassFoo = Foo<SomeBaseClass>();
  2. var extenderFoo = Foo<Extender>();

这时候也可以指定无参数的泛型,这时无参数泛型的类型则为 Foo<SomeBaseClass>

  1. var foo = Foo();
  2. print(foo); // Instance of 'Foo<SomeBaseClass>'

将非 SomeBaseClass 的类型作为泛型参数则会导致编译错误:

  1. var foo = Foo<Object>();

使用泛型方法

起初 Dart 只支持在类的声明时指定泛型,现在同样也可以在方法上使用泛型,称之为 泛型方法

  1. T first<T>(List<T> ts) {
  2. // Do some initial work or error checking, then...
  3. T tmp = ts[0];
  4. // Do some additional checking or processing...
  5. return tmp;
  6. }

方法 first<T> 的泛型 T 可以在如下地方使用:

  • 函数的返回值类型 (T)。
  • 参数的类型 (List<T>)。
  • 局部变量的类型 (T tmp)。

你可以查阅 使用泛型函数 获取更多关于泛型的信息。

库和可见性

importlibrary 关键字可以帮助你创建一个模块化和可共享的代码库。代码库不仅只是提供 API 而且还起到了封装的作用:以下划线(_)开头的成员仅在代码库中可见。 每个 Dart 程序都是一个库,即便没有使用关键字 library 指定。

Dart 的库可以使用 包工具 来发布和部署。

如果你对 Dart 为何使用下划线而不使用 publicprivate 作为可访问性关键字,可以查看 SDK issue 33383

使用库

使用 import 来指定命名空间以便其它库可以访问。

比如你可以导入代码库 dart:html 来使用 Dart Web 中相关 API:

  1. import 'dart:html';

import 的唯一参数是用于指定代码库的 URI,对于 Dart 内置的库,使用 dart:xxxxxx 的形式。而对于其它的库,你可以使用一个文件系统路径或者以 package:xxxxxx 的形式。 package:xxxxxx 指定的库通过包管理器(比如 pub 工具)来提供:

  1. import 'package:test/test.dart';

备忘:

URI 代表统一资源标识符。

URL(统一资源定位符)是一种常见的 URI。

指定库前缀

如果你导入的两个代码库有冲突的标识符,你可以为其中一个指定前缀。比如如果 library1 和 library2 都有 Element 类,那么可以这么处理:

  1. import 'package:lib1/lib1.dart';
  2. import 'package:lib2/lib2.dart' as lib2;
  3. // Uses Element from lib1.
  4. Element element1 = Element();
  5. // Uses Element from lib2.
  6. lib2.Element element2 = lib2.Element();

导入库的一部分

如果你只想使用代码库中的一部分,你可以有选择地导入代码库。例如:

  1. // Import only foo.
  2. import 'package:lib1/lib1.dart' show foo;
  3. // Import all names EXCEPT foo.
  4. import 'package:lib2/lib2.dart' hide foo;

延迟加载库

延迟加载(也常称为 懒加载)允许应用在需要时再去加载代码库,下面是可能使用到延迟加载的场景:

  • 为了减少应用的初始化时间。
  • 处理 A/B 测试,比如测试各种算法的不同实现。
  • 加载很少会使用到的功能,比如可选的屏幕和对话框。

目前只有 dart2js 支持延迟加载 Flutter、Dart VM 以及 DartDevc 目前都不支持延迟加载。你可以查阅 issue #33118issue #27776 获取更多的相关信息。

使用 deferred as 关键字来标识需要延时加载的代码库:

  1. import 'package:greetings/hello.dart' deferred as hello;

当实际需要使用到库中 API 时先调用 loadLibrary 函数加载库:

  1. Future<void> greet() async {
  2. await hello.loadLibrary();
  3. hello.printGreeting();
  4. }

在前面的代码,使用 await 关键字暂停代码执行直到库加载完成。更多关于 asyncawait 的信息请参考异步支持

loadLibrary 函数可以调用多次也没关系,代码库只会被加载一次。

当你使用延迟加载的时候需要牢记以下几点:

  • 延迟加载的代码库中的常量需要在代码库被加载的时候才会导入,未加载时是不会导入的。
  • 导入文件的时候无法使用延迟加载库中的类型。如果你需要使用类型,则考虑把接口类型转移到另一个库中然后让两个库都分别导入这个接口库。
  • Dart会隐式地将 loadLibrary() 导入到使用了 deferred as *命名空间* 的类中。 loadLibrary() 函数返回的是一个 Future

实现库

查阅 创建依赖库包 可以获取有关如何实现库包的建议,包括:

  • 如何组织库的源文件。
  • 如何使用 export 命令。
  • 何时使用 part 命令。
  • 何时使用 library 命令。
  • 如何使用导入和导出命令实现多平台的库支持。

异步支持

Dart 代码库中有大量返回 FutureStream 对象的函数,这些函数都是 异步 的,它们会在耗时操作(比如I/O)执行完毕前直接返回而不会等待耗时操作执行完毕。

asyncawait 关键字用于实现异步编程,并且让你的代码看起来就像是同步的一样。

处理 Future

可以通过下面两种方式,获得 Future 执行完成的结果:

使用 asyncawait 的代码是异步的,但是看起来有点像同步代码。例如,下面的代码使用 await 等待异步函数的执行结果。

  1. await lookUpVersion();

必须在带有 async 关键字的 异步函数 中使用 await

  1. Future<void> checkVersion() async {
  2. var version = await lookUpVersion();
  3. // Do something with version
  4. }

备忘:

尽管异步函数可以处理耗时操作,但是它并不会等待这些耗时操作完成,异步函数执行时会在其遇到第一个 await 表达式(代码行)时返回一个 Future 对象,然后等待 await 表达式执行完毕后继续执行。

使用 trycatch 以及 finally 来处理使用 await 导致的异常:

  1. try {
  2. version = await lookUpVersion();
  3. } catch (e) {
  4. // React to inability to look up the version
  5. }

你可以在异步函数中多次使用 await 关键字。例如,下面代码中等待了三次函数结果:

  1. var entrypoint = await findEntryPoint();
  2. var exitCode = await runExecutable(entrypoint, args);
  3. await flushThenExit(exitCode);
  4. await *表达式的返回值通常是一个 Future 对象;如果不是的话也会自动将其包裹在一个 Future 对象里。 Future 对象代表一个“承诺”, `await \*表达式\*`会阻塞直到需要的对象返回。*

如果在使用 await 时导致编译错误,请确保 await 在一个异步函数中使用。例如,如果想在 main() 函数中使用 await,那么 main() 函数就必须使用 async 关键字标识。

  1. void main() async {
  2. checkVersion();
  3. print('In main: version is ${await lookUpVersion()}');
  4. }

备忘:

如上的例子使用了声明为 async 的函数 checkVersion(),但没有等待其结果。在实际的开发中,如果代码假设函数已经执行完成,则可能导致一些异步的问题。想要避免这些问题,请使用 unawaited_futures 提示规则

For an interactive introduction to using futures, async, and await, see the asynchronous programming codelab.

声明异步函数

异步函数 是函数体由 async 关键字标记的函数。

将关键字 async 添加到函数并让其返回一个 Future 对象。假设有如下返回 String 对象的方法:

  1. String lookUpVersion() => '1.0.0';

将其改为异步函数,返回值是 Future:

  1. Future<String> lookUpVersion() async => '1.0.0';

注意,函数体不需要使用 Future API。如有必要,Dart 会创建 Future 对象。

如果函数没有返回有效值,需要设置其返回类型为 Future<void>

关于 Future、asyncawait 的使用介绍,可以参见这个 codelab: asynchronous programming codelab

处理 Stream

如果想从 Stream 中获取值,可以有两种选择:

  • 使用 async 关键字和一个 异步循环(使用 await for 关键字标识)。
  • 使用 Stream API。详情参考 库概览

备忘:

在使用 await for 关键字前,确保其可以令代码逻辑更加清晰并且是真的需要等待所有的结果执行完毕。例如,通常不应该在 UI 事件监听器上使用 await for 关键字,因为 UI 框架发出的事件流是无穷尽的。

使用 await for 定义异步循环看起来是这样的:

  1. await for (varOrType identifier in expression) {
  2. // Executes each time the stream emits a value.
  3. }

*表达式* 的类型必须是 Stream。执行流程如下:

  1. 等待直到 Stream 返回一个数据。
  2. 使用 1 中 Stream 返回的数据执行循环体。
  3. 重复 1、2 过程直到 Stream 数据返回完毕。

使用 breakreturn 语句可以停止接收 Stream 数据,这样就跳出了循环并取消注册监听 Stream。

如果在实现异步 for 循环时遇到编译时错误,请检查确保 await for 处于异步函数中。 例如,要在应用程序的 main() 函数中使用异步 for 循环,main() 函数体必须标记为 async

  1. void main() async {
  2. // ...
  3. await for (final request in requestServer) {
  4. handleRequest(request);
  5. }
  6. // ...
  7. }

你可以查阅库概览中有关 dart:async 的部分获取更多有关异步编程的信息。

生成器

当你需要延迟地生成一连串的值时,可以考虑使用 生成器函数。Dart 内置支持两种形式的生成器方法:

  • 同步 生成器:返回一个 Iterable 对象。
  • 异步 生成器:返回一个 Stream 对象。

通过在函数上加 sync* 关键字并将返回值类型设置为 Iterable 来实现一个 同步 生成器函数,在函数中使用 yield 语句来传递值:

  1. Iterable<int> naturalsTo(int n) sync* {
  2. int k = 0;
  3. while (k < n) yield k++;
  4. }

实现 异步 生成器函数与同步类似,只不过关键字为 async* 并且返回值为 Stream:

  1. Stream<int> asynchronousNaturalsTo(int n) async* {
  2. int k = 0;
  3. while (k < n) yield k++;
  4. }

如果生成器是递归调用的,可是使用 yield* 语句提升执行性能:

  1. Iterable<int> naturalsDownFrom(int n) sync* {
  2. if (n > 0) {
  3. yield n;
  4. yield* naturalsDownFrom(n - 1);
  5. }
  6. }

可调用类

通过实现类的 call() 方法,允许使用类似函数调用的方式来使用该类的实例。

隔离区

大多数计算机中,甚至在移动平台上,都在使用多核 CPU。为了有效利用多核性能,开发者一般使用共享内存的方式让线程并发地运行。然而,多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。

为了解决多线程带来的并发问题,Dart 使用 isolate 替代线程,所有的 Dart 代码均运行在一个 isolate 中。每一个 isolate 有它自己的堆内存以确保其状态不被其它 isolate 访问。

所有的 Dart 代码都是在一个 isolate 中运行,而非线程。每个 isolate 都有一个单独的执行线程,并且不与其他的 isolate 共享任何可变对象。

你可以查阅下面的文档获取更多相关信息:

Typedefs

类型别名是引用某一类型的简便方法,因为其使用关键字 typedef,因此通常被称作 typedef。下面是一个使用 IntList 来声明和使用类型别名的例子:

  1. typedef IntList = List<int>;
  2. IntList il = [1, 2, 3];

类型别名可以有类型参数:

  1. typedef ListMapper<X> = Map<X, List<X>>;
  2. Map<String, List<String>> m1 = {}; // Verbose.
  3. ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

版本提示: Before 2.13, typedefs were restricted to function types. Using the new typedefs requires a language version of at least 2.13.

针对函数,在大多数情况下,我们推荐使用 内联函数类型 替代 typedefs。然而,函数的 typedefs 仍然是有用的:

  1. typedef Compare<T> = int Function(T a, T b);
  2. int sort(int a, int b) => a - b;
  3. void main() {
  4. assert(sort is Compare<int>); // True!
  5. }

元数据

使用元数据可以为代码增加一些额外的信息。元数据注解以 @ 开头,其后紧跟一个编译时常量(比如 deprecated)或者调用一个常量构造函数。

Dart 中有两个注解是所有代码都可以使用的: @deprecated@Deprecated@override。你可以查阅 扩展一个类 获取有关 @override 的使用示例。下面是使用 @deprecated 的示例:

  1. class Television {
  2. /// Use [turnOn] to turn the power on instead.
  3. @Deprecated('Use turnOn instead')
  4. void activate() {
  5. turnOn();
  6. }
  7. /// Turns the TV's power on.
  8. void turnOn() {...}
  9. // ···
  10. }

可以自定义元数据注解。下面的示例定义了一个带有两个参数的 @todo 注解:

  1. library todo;
  2. class Todo {
  3. final String who;
  4. final String what;
  5. const Todo(this.who, this.what);
  6. }

使用 @Todo 注解的示例:

  1. import 'todo.dart';
  2. @Todo('seth', 'make this do something')
  3. void doSomething() {
  4. print('do something');
  5. }

元数据可以在 library、class、typedef、type parameter、 constructor、factory、function、field、parameter 或者 variable 声明之前使用,也可以在 import 或 export 之前使用。可使用反射在运行时获取元数据信息。

注释

Dart 支持单行注释、多行注释和文档注释。

单行注释

单行注释以 // 开始。所有在 // 和该行结尾之间的内容均被编译器忽略。

  1. void main() {
  2. // TODO: refactor into an AbstractLlamaGreetingFactory?
  3. print('Welcome to my Llama farm!');
  4. }

多行注释

多行注释以 /* 开始,以 */ 结尾。所有在 /**/ 之间的内容均被编译器忽略(不会忽略文档注释),多行注释可以嵌套。

  1. void main() {
  2. /*
  3. * This is a lot of work. Consider raising chickens.
  4. Llama larry = Llama();
  5. larry.feed();
  6. larry.exercise();
  7. larry.clean();
  8. */
  9. }

文档注释

文档注释可以是多行注释,也可以是单行注释,文档注释以 /// 或者 /** 开始。在连续行上使用 /// 与多行文档注释具有相同的效果。

在文档注释中,除非用中括号括起来,否则分析器会忽略所有文本。使用中括号可以引用类、方法、字段、顶级变量、函数和参数。括号中的符号会在已记录的程序元素的词法域中进行解析。

下面是一个引用其他类和成员的文档注释:

  1. /// A domesticated South American camelid (Lama glama).
  2. ///
  3. /// Andean cultures have used llamas as meat and pack
  4. /// animals since pre-Hispanic times.
  5. ///
  6. /// Just like any other animal, llamas need to eat,
  7. /// so don't forget to [feed] them some [Food].
  8. class Llama {
  9. String? name;
  10. /// Feeds your llama [food].
  11. ///
  12. /// The typical llama eats one bale of hay per week.
  13. void feed(Food food) {
  14. // ...
  15. }
  16. /// Exercises your llama with an [activity] for
  17. /// [timeLimit] minutes.
  18. void exercise(Activity activity, int timeLimit) {
  19. // ...
  20. }
  21. }

在生成的文档中,[feed] 会成为一个链接,指向 feed 方法的文档, [Food] 会成为一个链接,指向 Food 类的 API 文档。

解析 Dart 代码并生成 HTML 文档,可以使用 Dart 的文档生成工具 dart doc。关于生成文档的示例,请参考 Dart API documentation 查看关于文档结构的建议,请参考文档: Guidelines for Dart Doc Comments.