前言

如果你正在寻找一个跨平台的原生应用开发方案,你可能已经了解过 Flutter,ReactNative 等。 Dart 是 Flutter 应用的开发语言,它专为客户端优化,支持自动内存管理和面向对象编程。 这里的跨平台指的是一套代码,可发布成 iOS、Android、macOS、Linux、Windows 及 Web 应用。 原生应用是指代码最终被编译为平台相关的机器码;对 web 而言,对应的是 javascript+WebAssembly。

mindmap
  root((Dart))
    (自动内存管理)
    (面向对象编程)
    (客户端优化)

如何你初学编程,一下子听到这么多概念,也许还没开始写一行代码就已经开始糊涂了,没有关系。 要知道,计算机编程就像英语一样,要掌握它,除了需要兴趣之外,还需要良好的思维模式以及坚持不懈的练习。 希望你在学习本课程时,打开代码编辑器,跟着一起练习,为以后编程打下坚实的基础。在你掌握了一门编程语言之后,再去学其他编程语言就容易多了。

读者范围

本课程将详细介绍 Dart 3 编程语言 ,从基本语法与控制结构到异步编程,从面向对象编程到编码准则;课程内容由浅入深,既有理论讲解又有代码示例 。相信无论你是编程新手还是具有一定经验的程序员,你都可以从本课程中获益。

课程内容简介

第 1 章,从 0 开始搭建开发环境,学习 dart 变量与数据类型及操作符。

第 2 章,控制流、函数及异常处理。

第 3 章,Dart 强大的模式匹配功能。

第 4-5 章,Dart 的 面向对象编程(OOP),第 4 章学习用于封装信息的 class(类)的构造,第 5 章学习类的继承及扩展。

第 6 章,集合与泛型。

第 7 章,异步编程。

第 8 章,单元测试。

附录包括 dart 命令行工具、编码准则、Dart 命令行及服务端编程,以及常用 package 介绍。

本课程中的代码可免费下载,使用的 dart 版本为 3.8+。

最后,希望本课程能对你的 dart 学习之旅助一臂之力,祝你学习愉快。

Hello Dart: 搭建开发环境

搭建开发环境是我们编程的起点,新手有可能卡在这一步。如果你是经验丰富的程序员,可以直接跳过。搭建开发环境需要考虑的两个主要问题是开发机的操作系统与网络环境。

我们的开发电脑的操作系统可能是下列之一:

  • macOS Sonoma (14), Ventura (13), Monterey (12)
  • Windows 10,11
  • ChromeOS
  • Linux
    • Debian stable
    • Ubuntu LTS
    • 其他,如Manjaro,CentOS Stream,Linux Mint

这里推荐的操作系统是 MacOS 或 Linux ,因为它们具有更丰富的开发工具及各种命令,极大的方便了软件开发。如果你使用的计算机网络位于中国,由于其网络特殊性,可能需要使用相关资源(各软件包/工具/在线服务)的镜像。

Dart.dev

如果你只是想快速搭建Dart或Flutter开发环境,可以跳过本段。

  1. 首先我们去Dart官网 https://dart.dev ,点击右上角的 Get Dart

注意页面上的提示,如果你已经安装或打算安装Flutter SDK,可以跳过该向导,因为Flutter SDK包含了Dart SDK。 我们学习Dart最主要的目的就是使用Flutter框架,编写跨平台App。因此,我们这里直接安装Flutter SDK。

  1. 点击 install the Flutter SDK, 我们来到了Flutter SDK 安装向导页。

操作系统及网络环境的不同,会导致搭建Dart开发环境的方式也有所不同。但是这些方式有着类似的步骤。本文选取 中国网络环境下的Linux系统 对这些步骤进行说明。

在中国网络环境下使用 Flutter

有必要解释一下镜像或镜像站点。 Flutter SDK (软件开发工具包)或 Dart SDK 以及 dart package(软件包)(发布于pub.dev网站),是我们开发Flutter或Dart程序必备的,但由于中国网络的特殊性,导致许多中国开发者无法直接(从官方服务器,由谷歌维护)获取这些SDK或package(或者下载速度极慢),所以我们就去官方服务器的镜像站点去下载。这些位于中国的镜像站点会定时与官方服务器同步,略有延迟。

  1. 在电脑上打开 terminal (终端命令行界面),以上海交通大学*nix用户组镜像为例,配置镜像
export PUB_HOSTED_URL=https://mirror.sjtu.edu.cn/dart-pub
export FLUTTER_STORAGE_BASE_URL=https://mirror.sjtu.edu.cn
  1. 从镜像站点下载 Flutter 压缩包

以 Flutter 3.32.2 为例, 使用如下命令下载

wget -c -O flutter_linux_3.32.2-stable.tar.xz  $FLUTTER_STORAGE_BASE_URL/flutter_infra_release/releases/stable/linux/flutter_linux_3.32.2-stable.tar.xz

注: macOS与windows对应的下载地址分别为

$FLUTTER_STORAGE_BASE_URL/flutter_infra_release/releases/stable/macos/flutter_macos_3.32.2-stable.zip

$FLUTTER_STORAGE_BASE_URL/flutter_infra_release/releases/stable/windows/flutter_windows_3.32.2-stable.zip
  1. 创建一个文件夹,用于安装Flutter, 例如 ~/dev
mkdir ~/dev
cd ~/dev
  1. 将 flutter_linux_3.32.2-stable.tar.xz 拷贝到当前目录,然后解压
tar -xf flutter_linux_3.32.2-stable.tar.xz
  1. 将 Flutter 添加到PATH 环境变量中
export PATH="$PWD/flutter/bin:$PATH"
  1. 运行 flutter doctor 来验证安装
$ flutter doctor

Flutter assets will be downloaded from https://mirror.sjtu.edu.cn. Make sure you trust this
source!
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.32.2, on Manjaro Linux 6.6.63-1-MANJARO, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Chrome - develop for the web
[✓] Linux toolchain - develop for Linux desktop
[✓] Android Studio (version 2023.2)
[✓] IntelliJ IDEA Community Edition (version 2024.3)
[✓] VS Code (version unknown)
    ✗ Unable to determine VS Code version.
[✓] Connected device (2 available)
[✓] Network resources

• No issues found!

查看 Flutter 及 Dart 版本

$ flutter --version                                                        ✔ 
Flutter 3.32.2 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 8defaa71a7 (4 days ago) • 2025-06-04 11:02:51 -0700
Engine • revision 1091508939 (9 days ago) • 2025-05-30 12:17:36 -0700
Tools • Dart 3.8.1 • DevTools 2.45.1

$ dart --version                                                           ✔ 
Dart SDK version: 3.8.1 (stable) (Wed May 28 00:47:25 2025 -0700) on "linux_x64"

配置环境变量

在上述过程中, export 设置的环境变量,仅对当前terminal有效;要永久设置这些值, 将这三条 export 指令添加到首选 shell 使用的 *rc 或 *profile 文件中,例如

cat <<EOF >> ~/.zshrc
export PUB_HOSTED_URL=https://mirror.sjtu.edu.cn/dart-pub
export FLUTTER_STORAGE_BASE_URL=https://mirror.sjtu.edu.cn
export PATH="\$HOME/dev/flutter/bin:\$PATH"
EOF

使用 tail 命令查看刚才写入的内容

tail -3 ~/.zshrc

Hello Dart

下面我们来创建第一个Dart.

$ dart create hellodart         
Creating hellodart using template console...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  bin/hellodart.dart
  lib/hellodart.dart
  test/hellodart_test.dart

执行完dart create命令之后, 我们得到了一个hello world project.

下面我们来运行一下

$ dart run                        ✔ 
Building package executable... 
Built hellodart:hellodart.
Hello world: 42!

编译为可执行程序

$ dart compile exe bin/hellodart.dart

$ ./bin/hellodart.exe             ✔ 
Hello world: 42!

dart 是一个强大的命令,关于它的基本使用,本课程将其放在附录中说明。

安装IDE (集成开发环境,代码编辑器)

有几款优秀且跨平台的IDE可供选用:

下载安装好IDE后,再打开IDE安装Dart扩展。

另外,dartpad.dev是一个在线编辑器,可用于临时测试一些代码。

如果你看到了这里,并且也跟着一起练习,那么恭喜你,你已经开启了Dart编程之旅。

附: 社区镜像站点

# 上海交通大学 *nix 用户组
export PUB_HOSTED_URL=https://mirror.sjtu.edu.cn/dart-pub;
export FLUTTER_STORAGE_BASE_URL=https://mirror.sjtu.edu.cn

# 清华大学 TUNA 协会
export PUB_HOSTED_URL=https://mirrors.tuna.tsinghua.edu.cn/dart-pub
export FLUTTER_STORAGE_BASE_URL=https://mirrors.tuna.tsinghua.edu.cn/flutter

# 中国 Flutter 社区 (CFUG)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

参考资料

变量与Null Safety

什么是变量

计算机编程语言里的变量(Variable)与数学公式里的变量类似。请看下面这段Dart代码:

// ex121
void main() {
  var x = 3;  // 1
  var (y, z) = (4, 5); // 2
  print('x = $x, y = $y, z = $z'); // 3 Output: x = 3, y = 4, z = 5
  print('x + y + z = ${x + y + z}'); // 4 Output: x + y + z = 12
}

这段简短的代码只有一个函数,那就是main,整个程序的入口。代码中的 // 是注释,是为了帮助人们阅读代码,Dart编译器会忽略它。

  1. 使用var关键字,声明变量x,并给x赋值为 3,Dart编译器自动推断其类型为 int (integer, 整数);
  2. 同时声明两个变量xy,分别赋值为4和5,这里使用了record的模式匹配(第3章内容,这里看不懂也没关系);
  3. 分别将xyz的值打印(显示)到控制台(标准输出设备 stdout);
  4. 计算 x + y + z 并打印。

第1句也可以写成 int x = 3; 也等同于

int x;
x = 3;

第2句也可以写成

  var y= 4, z = 5;

第3、4两句中的 $x, $y, $z 以及 ${x + y + z} 为 字符串插值,非常直观和方便。

var x 声明了一个变量,它的标识符为x; 标识符,通俗地说就是给变量、函数、类、方法等起个名字,方便使用,就像我们给宠物取个名,方便呼唤。Dart语言的标识符可以是下列字符的组合:

  • 字母
  • 数字
  • _$

但不能以数字开头。这些都是合法的标识符: practiceMakesPerfect, _imPrivate, _$GeneratedCode, $happy365。Dart里以下划线(_ )开头的变量是私有变量。

var修饰的变量 ,可以后续更改,比如

// ex123
void main() {
  var x = 3;
  x = 30;
  print('x = $x'); // Output: x = 30
}

变量除了可以表示数字,还可以表示文本(字符串)等. 例如

// ex124
void main(){
  var name = 'Dart';
  print('Hello, $name!'); // Output: Hello, Dart!
}

这里的数字、字符串等,就是所谓的数据类型(本章第4节内容,这里不再展开)。

使用 var 声明变量

对于 var x = 3; 也可以写成 int x = 3,你可能有个疑问,哪种情况下用哪种方式呢?官方的建议是,多数情况下,我们应该使用 var (而非实际类型) 声明变量,这样做除了可以使代码更短之外,还能提高代码的可读性。

如果我们仅仅声明变量(不赋值),应该写出实际的类型,因为这时候Dart无法推断出变量的数据类型,如

int x;

如果我们这样写

var x;

Dart编译器就认为 x 的类型为 dynamic ,这种类型可以表示任何数据,通常情况下我们应该避免这样做,因为Dart的静态类型检查对dynamic无效(可能暗藏bug)。

使用 late 延迟初始化

有时候我们希望延迟变量的初始化(特别在Flutter的StatefulWidget中),这时就用到 late 关键字, 例如

// ex125
void main() {
  late String action = 'go camping';
  var isReady = true; // it may take some time to prepare
  if (isReady) {
    print("Let's $action!"); // Output: Let's go camping!
  }
}

这里先了解下,第4章会有更多关于 late 的讨论。

Null Safety

NNBD

从Dart 2.10 开始,变量默认非 null(non-nullable by defaullt, NNBD);null 表示空值。 比如下面这段代码无法编译

// ex126
void main() {
  int x;
  print('x = $x'); 

  // Error: Non-nullable variable 'x' must be assigned before it can be used.
  // print('x = $x'); 
  //             ^
}

当我们尝试编译上面这段代码时,出现编译错误,告诉我们在使用变量x之前必须初始化它。

如果要使用null, 可以像下面这样:

// ex127
void main() {
  int? x;
  print('x = $x'); // Output: x = null
}

这里的 int? x 表示变量x 的初始值为null, x 是一个nullable变量。

注: 熟悉Java的朋友想必对NullPointerException (NPE) 并不陌生,这个Exception是一个运行时异常;为了提高代码的健壮性,经常要对方法的入参进行非空检查,否则就很容易遭遇NPE(这个问题有时难以排查,尤其在大型软件项目里)。例如下面这段Java代码

void doSomething(String str) {
  if (str == null){
    throw new IllegalArgumentException("doSomething: str cannot be null");
  }
  // Do something
}

我想这便是Dart NNBD的原因。

访问nullable变量

有几个nullable变量相关的访问/赋值,有必要介绍一下,你以后再看到下面这些符号就不会感到陌生了。

????=!

// ex128
void main() {
  int? x;
  var y = x ?? 10; // 1
  var z = y ?? 20; // 1a
  print('y = $y, z = $z'); // Output: y = 10, z = 10

  int? y2;
  y ??= 100; // 2
  y2 ??= 100; // 2a
  print('y = $y, y2 = $y2'); // Output: y = 10, y2 = 100

  var a = y!; // 3
  print('a = $a'); // Output: a = 10

  var b = x!; // 3a
  print('b = $b');
  // Unhandled exception:
  // Null check operator used on a null value
}
  1. 如果 xnully赋值为10, 否则将x的值赋给y
  2. 如果 ynully赋值为100,否则啥也不做;
  3. 断言y一定非空(null),并将y的值赋给a;如果ynull,在程序运行时会出现一个Null check异常(见3a)。

请对比 1和1a, 2和2a,以及3和3a的结果。

?.?[]

// ex129
void main() {
  var abc = 'ABCD';
  String? def;

  var first = abc?[0]; // 1
  var second = def?[0]; // 1a
  print('first = $first, second = $second'); // Output: first = A, second = null

  var len1 = abc?.length; // 2
  var len2 = def?.length; // 2a
  print('len1 = $len1, len2 = $len2'); // Output: len1 = 4, len2 = null

  var len3 = def?.length ?? 0; // 3
  print('len3 = $len3'); // Output: len3 = 0
}
  1. 如果abcnullfirstnull, 否则将abc[0]的值赋给first
  2. 如果abcnulllen1null, 否则将abc.length的值赋给len1
  3. 这是一个综合的例子,同时使用了 ?.?? 符号。

注:本例中的 abc[0] 返回第0个(下标从0开始)unicode字符,abc.length返回abc的长度。

...?

// ex12a
void main() {
  var arr = [1, 2, 3];
  var arr2 = [...arr, 4, 5]; // 1
  print('arr2 = $arr2'); // Output: arr2 = [1, 2, 3, 4, 5]

  var arr3 = [...?arr, 6, 7]; // 1a
  print('arr3 = $arr3'); // Output: arr3 = [1, 2, 3, 6, 7]

  List<int>? arr4;
  var arr5 = [...?arr4, 8, 9]; // 2
  print('arr5 = $arr5'); // Output: arr5 = [8, 9]

  List<int>? arr6 = []; // 3
  var arr7 = [...arr6, -1, -2]; // 3a
  print('arr7 = $arr7'); // Output:  arr7 = [-1, -2]
}
  1. arr是一个列表(数组), ...arr将展开arr, 此行代码将arr以及 4, 5 一起组成一个新的列表,并将其赋值给arr2;
  2. 本行代码 与 1 类似, 区别在于,这里必须使用 ...?arr4 执行展开操作,否则无法编译通过;
  3. arr6arr4一样,是一个nullable变量,但其初始值为一个空的列表(不是null),就可以用...对其进行展开,可见Dart编译器非常智能。

参考资料

finalconst 及wildcard

final

声明为final的变量,只能被赋值一次。 如果尝试修改已经赋值过的final变量,会引起编译错误。

// ex131.dart
void main() {
  final myPet = 'Doggy'; // 1
  myPet = 'Kitty'; // 2
  // Error: Can't assign to the final variable 'myPet'.
  // myPet = 'Kitty'; // 2
  // ^^^^^
}
  1. 声明一个final变量myPet, 并为其赋值;
  2. 尝试修改myPet的值,引起编译错误, 告诉我们不能给final 变量 myPet 赋值。

late final

late final 这两个关键字经常一起出现,这里构建了一个简单的例子:

// ex132.dart
void main() {
  final width = 3.0; // 1
  final height = 4.0; // 1a
  late final double perimeter; // 2
  // ... (Do somehting else)
  perimeter = 2 * (width + height); // 3
  print('width: $width, height: $height, perimeter: $perimeter'); // 4
  // Output: width: 3.0, height: 4.0, perimeter: 14.0
}
  1. 声明final变量 widthheight ,并分别赋值为3.0与4.0,Dart自动推断其数据类型为double (浮点数);
  2. 使用 late final 声明一个变量perimeter,在这里它表示一个矩形的周长,同时显示指定这个变量的数据类型为double,所以这里有3个关键字late final double,它们一起修饰变量perimeter;
  3. 计算变量 perimeter的值,公式为(长+宽)×2;
  4. 使用字符串插值,打印相关变量。

这个例子似乎看不出 late final 有什么特别之处,这里只是让你熟悉一下,在面向对象编程(OOP)的章节还会继续讨论它。

final 的兄弟 const

const关键字用于:

  • 声明一个常量,常量具有编译时(compile-time)不可变性,这里的编译时是与运行时(runtime)相对应的;
  • 创建常量值(constant values);
  • 定义const构造器,该构造器用于创建常量值。

后面两点与OOP有关,这里提前了解下。

声明一个常量

试图修改常量会引起编译错误,例如

// ex133.dart
void main(){
  const score = 50; // 1
  score = 80; // 2
  // Error: Can't assign to the const variable 'score'.
  // score = 80; // 2
  // ^^^^^
}

创建常量值

任何变量都可以有常量值。

// ex134.dart
void main() {
  final luckyNumbers = [5, 6]; // 1
  luckyNumbers.add(8); // 2
  print('luckyNumbers: $luckyNumbers');
  // Output: luckyNumbers: [5, 6, 8]

  const myFriends = ['Alice', 'Bob']; // 3
  myFriends.add('Charlie'); // 4
  // Unhandled exception:
  // Unsupported operation: Cannot add to an unmodifiable list;
}
  1. 声明一个final 列表 luckyNumbers并对其初始化,包含5、6两个数字;
  2. luckyNumbers添加数字8,然后打印出来,luckyNumbers 现在包含了5、6、8三个数字; 这说明了虽然final修饰的luckyNumbers本身(引用)不可更改,但它引用的列表是可以改变的;
  3. 声明一个const列表myFriends,初始值包含"Alice和"Bob"两个字符串;
  4. myFriends添加一个新元素"Charlie",这引起了一个运行时异常,告诉我们不能向不可变列表添加新的元素,可见constfinal具有更强的不可变性。

定义const构造器

构造器是OOP里的概念,const构造用于创建常量值。请看下面这个示例:

// ex135.dart
class Pet { // 1
  final String name; // 1a
  const Pet(this.name); // 2
}

void main() {
  var myPet = Pet('Doggy'); // 3
  myPet = Pet('Little Doggy'); // 3a
  const yourPet = Pet('Kitty'); // 4

  print('myPet: ${myPet.name}, yourPet: ${yourPet.name}');
  // Output: myPet: Little Doggy, yourPet: Kitty
}
  1. class关键字用于声明一个名叫Pet的类 ,类封装了数据和行为(方法)(第4-5章专门讨论OOP); class Pet 有一个final成员变量叫做 name ;
  2. 这是Pet的const构造器;
  3. 通过调用Pet的构造器,得到一个Pet的实例(对象)myPet,随后又将myPet指向另一个Pet对象(3a);
  4. 通过调用Pet的const构造器,创建Pet的常量对象 yourPet,这行代码等于
  const yourPet = const Pet('Kitty'); // 4

在常量上下文中,Pet('Kitty')之前省略了const关键字。

常量对象

const Pet('Kitty') Pet('Kitty')是不一样的,前者创建(实例化)了一个常量对象,而后者创建的是非常量对象(除非是在常量上下文中)。

// ex136.dart
void main() {
  const pet1 = Pet('Rabbit'); // 1
  var pet2 = const Pet('Rabbit'); // 2
  final pet3 = Pet('Rabbit'); // 3

  print('${pet1 == pet2}'); // Output: true
  print('${pet1 == pet3}'); // Ouptput: false
}

class Pet {
  final String name;
  const Pet(this.name); 
}

这里的 == 操作符,用于测试左右两边的操作数是否相等。

Wildcard(_

自 Dart 3.7 开始,以 _ 命名 的变量或参数,是一个通配符(wildcard ),它的值会被丢弃。同一个命名空间里多次声明 _ 不会冲突。例如

// ex137.dart
void main() {
  var _ = 10; // 1
  var _ = 'Hello'; // 2
  print('Hello, Dart! $_'); // 3
  // Error: Undefined name '_'.
  // print('Hello, Dart! $_');
}

上面这段代码比较简单,不过多解释。

通配符出现的地方:

  • 变量声明, 如 var _ = 1;
  • for 循环变量 如 for (var _ in list) {}
  • catch 语句参数, 如
try {
  throw 'something bad';
} catch (_) {
  print('oops');
}
  • 函数参数,如 list.where((_) => true);
  • 泛型类型及函数类型参数, 如
class T<_> {}
void genericFunction<_>() {}

for循环、函数、catch语句、泛型等概念将在后续章节陆续介绍,这里关于通配符有个印象就行。

更多的示例,可查阅官方文档

最佳实践

实践中应遵循“能用 final 不用 var,能用 const 不用 final”的黄金原则。

下面重点说下const对提高程序性能的至关作用。

编译时常量 vs 运行时对象

  • 普通对象(final 或无修饰)在程序运行到那一行时才在堆(Heap)上分配内存。
  • const 对象在编译时就已确定。Dart 编译器会将这些常量放入一个特殊的“常量池”中。无论你的代码运行多少次,该常量在内存中永远只有一份。

const 对 Flutter 渲染性能的巨大贡献

  • 避免不必要的 Rebuild:当父 Widget 触发重新构建(Rebuild)时,如果子 Widget 带有 const 修饰,Flutter 会通过内存地址判断它是同一个实例,从而跳过对该子 Widget 的构建和比对过程。

  • 减轻 GC(垃圾回收)压力:频繁地销毁和创建临时 Widget 会频繁触发 GC。const 对象生命周期贯穿程序始终,不会被回收,从而减少了 GC 引起的微小卡顿。

参考资料

数据类型

通常计算机编程语言里的数据类型可以分为基础数据类型与复合数据类型两大类。Dart的基础数据类型包括:

复合数据类型是基础数据类型的组合。Dart的复合数据类型包括:

Dart还有如下特殊的数据类型:

第2章将讨论函数,第4章将讨论类,第6章将讨论列表(List)、集合(Set)、映射(Map)。

数字

Dart的数字包括64-bit整数 int 和 64-bit浮点数 double。 int的取值范围为 -263 至 to 263 -1。 double是双精度浮点数,遵循 IEEE 754 标准。

Dart是一门OOP编程语言,int和double是num的子类,因此它们有着一些共同的方法,例如:

  • parse(string)
  • abs()
  • ceil()
  • toString()

下面的代码声明了一个整数radius 和一个浮点数 pi

// ex141.dart
void main() {
  int radius = 5;
  double pi = 3.1416;
  print('area=${radius * radius * pi}'); 
  // Output: area=78.53999999999999
}

除了使用十进制书写整数外,还可以用十六进制。可以使用科学计数法书写浮点数。例如:

// ex142.dart
void main() {
  int radius = 0x10; // hexadecimal
  int radius2 = 16; // decimal
  double pi = 0.31416e1;

  print('radius==radius2: ${radius == radius2}');
  // Output: radius==radius2: true

  print('radius=$radius area=${radius * radius * pi}');
  // Output: radius=16 area=804.2496
}

但是Dart并不直接支持书写八进制数。你可以用 octal 包来书写八进制数。

// ex143.dart
// dart pub add octal
import 'package:octal/octal.dart'; 

void main() {
  int decimalValue = octal(123); // 83 in decimal
  print(decimalValue); // Output: 83
  print(decimalValue.toRadixString(8)); // Output: 123
}

书写数字时,可以在数字中间添加下划线来分节,这一点非常实用。

// ex14g.dart
void main() {
  print(10_000); // Output: 10000
  print(1_2.000_123); // Output: 12.000123
  print(0x1_0); // 0utput: 16
}

字符串(String)

Dart中的String是一个UTF-16值的有序序列,写在一对单引号或双引号里。一个非常nice的功能是字符串插值,即在字符串里使用 ${expr}

// ex144.dart
void main() {
  var str = "It's a beautiful day!";
  var str2 = 'It\'s a beautiful day!'; 
  print(str == str2); // Output: true

  var num = 5;
  print('There are $num apples.'); // Output: There are 5 apples.
  print('There are ${num + 2} oranges.'); // Output: There are 7 oranges.
}

上例中str2 写在一对单引号中,该行中的 \ 为转义符,\'表示一个单引号。

两个字符串写在一起,中间可以使用一个+符号(但通常予以省略), 就表示将这两个字符串顺序拼接在一起,形成一个新的字符串。因此对于多行文本,可以这样写:

  // ex145.dart
  var str =
      'A: Nice to meet you!\n'
      'B: Nice to meet you, too!';

这里的\n是换行符。书写多行文本更方便的做法是将文本写在一对三引号('''""")中,例如:

// ex146.dart
void main() {
  var minAge = 18;
  var query =
      '''
SELECT id, name, age
FROM users
WHERE age >= $minAge
''';
  print(query);
}

布尔(bool)

布尔类型只有2个值,truefalse,且它们都是编译时常量。

// ex147.dart
void main() {
  var isGreater = 43 > 34;
  print('isGreater: $isGreater'); // Output: isGreater: true
  print('2 < -3: ${2 < -3}'); // Output: 2 < -3: false
}

符文(Rune)

符文代表unicode文字系统里的一个符号。unicode为世界上每一个文字或符号分配了一个数字,称之为code point。unicode 最多可支持 1,114,112 个code point,通过 17 个 16 位平面实现,每个平面可支持 65,536 个不同的code point。 unicode常见的编码方式(将code point映射为二进制数)有UTF-8、UTF-16和UTF-32。 Dart采用UTF-16编码,每个code unit为16位(2个字节),每个符文占用1个(0-65,535 code point)或2个code unit(65,536及以上)。对字符编码感兴趣的同学,请继续去了解下ASCII、UTF-8、UTF-16和UTF-32、GBK等。

Dart的String(字符串)本质上是code unit的序列。

// ex148.dart
void main() {
  var s = "笑\u7b11lol😆\u{1f606}"; // 1
  print(s); // 2 Output: 笑笑lol😆😆
  print('len: ${s.length}'); // 3 Output: len: 9

  print('code units: ${s.codeUnits}'); // 4
  // Output: code units: [31505, 31505, 108, 111, 108, 55357, 56838, 55357, 56838]

  print('runes: ${s.runes}'); // 5
  // Output: runes: (31505, 31505, 108, 111, 108, 128518, 128518)

  print(31505.toRadixString(16)); // 6 Output: 7b11
  print(128518.toRadixString(16)); // 7 Output: 1f606
}
  1. 声明变量s,注意这里\u7b11就等于这个字符,\uHHHH表示\u后面是code point的十六进制表示HHHH;如果该十六进制数不是4位,就必须位于一堆花括号{}中,如\u{1f606}就等于😆这个emoji符号;
  2. 打印 s,从输出结果可以看出 s包含了7个符文(字符);
  3. 打印s的长度,结果为9, 可见是code unit的个数,下面一行代码印证了这一点;
  4. 打印s的code point 序列;
  5. 打印s的符文(字符)序列;
  6. 第6-7行代码分别显示31505和128518对应的十六进制表示,以方便分析。

请仔细查看code units 与 runes 并进行对比,这将帮助你理解Rune(符文)和String(字符串)。

使用 characters

下面这个示例同时演示了charactersStringBuffer类的使用。charactersString类添加了一个扩展方法(第5章内容)get charactersStringBuffer是拼接字符串的高效方式。代码中使用的 for-each 循环将在下一章节介绍,这里先了解下。

//ex149.dart
//dart pub add characters
import 'package:characters/characters.dart';

void main() {
  const s = "笑一笑十年少😆"; // 1
  final sb = StringBuffer(); // 2 
  for (var ch in s.characters) { // 3
    sb.write(ch);
    sb.write(' ');
  }
  print(sb);
}

记录(Record)

Dart 3.0 引入了记录这一数据类型。记录是匿名的、不可变的聚合类型,是异质数据的集合。

// ex14a.dart
void main() {
  var record = ('class 1', name: 'Alice', id: 1, 'good student'); // 1
  print(record); // 2 Output: (class 1, good student, id: 1, name: Alice)
  print(record.$1); // 3 Output: class 1
  print(record.$2); // Output: good student
  print(record.id); // 4 Ouput: 1
  print(record.name); // Output: Alice

  // record.id = 2; // 5
  // Error: The setter 'id' isn't defined for the class '(String, String, {int id, String name})'.
  // Try correcting the name to the name of an existing setter, or defining a setter or field named 'id'.
  //   record.id = 2;
  //          ^^
}
  1. 声明一个变量record并赋值,如你所见,记录有位置字段和命名字段(如这里的idname);
  2. 打印 record,注意观察字段的显示顺序;
  3. 打印 record 的第1个位置字段record.$1 ;
  4. 打印 record 的命名字段 record.id;
  5. 修改 recordid,遭遇编译错误,这说明了记录是不可变的,同时从错误信息中可以看出,记录是一种特殊的匿名类。

记录类型

记录(Record)没有明确的类型声明,因为它是匿名的,但记录是有形状(Shape)的。本例中 record的形状为(String, String, {int id, String name});在一对花括号({})里的int id, String name 为命名字段 idname,其类型分别为intString;没有花括号包围的便是位置字段。

// ex14b.dart
void main() {
  (int x, int y, int z) point = (1, 2, 3);
  var color = (1, 2, 3);
  print(point); // Output: (1, 2, 3)
  print(point == color); // Output: true

  ({int x, int y, int z}) point2 = (x: 1, y: 2, z: 3);
  var color2 = (r: 1, g: 2, b: 3);
  print(point2); // Output: (x: 1, y: 2, z: 3)
  print(color2); // Output: (b: 3, g: 2, r: 1)
  print(point2 == color2); // Output: false
}

pointcolor具有相同的形状和值,因此他们是相等的,而point2color2的形状不同,自然就不相等。

解构与变量交换

记录的解构与变量交换非常实用。

// ex14c.dart
void main() {
  var (x, y) = (1, 2); // 1
  print('x=$x y=$y'); // Output: x=1 y=2

  (y, x) = (x, y); // 2
  print('x=$x y=$y'); // Output: x=2 y=1

  var (:name, :age) = (name: 'Bob', age: 20); // 3
  print('name=$name b=$age'); // Output: name=Bob b=20

  var (x: a, y: b) = (x: 3, y: 4); // 4
  print('a=$a b=$b'); // Output: a=3 b=4
  
  var (who, _, :nickname, fav: _) = (
    'Robert',
    'Naughty boy',
    nickname: 'Bob',
    fav: 'play the guitar',
  ); // 5
  print('who=$who nickname=$nickname'); // Output: who=Robert nickname=Bob
}
  1. 解构记录(1,2)至局部变量xy,这里使用了模式匹配(第3章内容);
  2. 交换xy的值;
  3. 使用冒号(:)语法,解构命名记录 (name: 'Bob', age: 20)至局部变量nameage ;
  4. 解构命名记录 (x: 3, y: 4)至局部变量ab ;
  5. 这是一个综合例子,请运用已学知识自行分析。

枚举(Enum)

枚举是一种用于表示固定数量的常量值的特殊类。使用关键字enum声明一个枚举。

// ex14d.dart
enum Color { red, green, blue }

void main() {
  print(Color.red); // Output: Color.red
  print(Color.blue.name); // Output: blue
  print(Color.blue.index); // Output: 2
}

每个枚举值都有个与之关联的数字,称之为index,该数字从0开始。枚举是一种特殊的类,因此它也可以定义字段(实例变量)和方法,只不过有一些限制,例如枚举的实例变量必须声明为final。第4章会更详细地讨论枚举。

符号(Symbol)

符号对象表示 Dart 程序中声明的操作符或标识符。一般极少用到,在此了解即可。

// ex14e.dart
void main() {
  print(#foo); // Output: Symbol("foo")
}

typedef

typedef关键字为数据类型取一个别名。

//ex14f.dart
typedef Point = ({int x, int y});
typedef VoidCallback = void Function();
typedef IntList = List<int>;

void main() {
  Point p = (x: 1, y: 2);
  IntList nums = [3, 4, 5];
}

小测试

以下程序的输出是什么?

// ch01004_quiz.dart
void main() {
  var point = (x: 1, y: 2, z: 3);
  ({num x, int y, int z}) point2 = (x: 1, y: 2, z: 3);
  print(point == point2);
}

参考资料

操作符

计算机程序的表达式由操作符和操作数构成。如1+2这个表达式中,+是操作符,数字12是操作数。如果表达式中只有一个操作数,它便是一元表达式;如果有两个操作数,它便是二元表达式;类似地还有三元表达式。一元表达式中的操作符为一元操作符。

Dart提供了非常丰富的操作符,且支持为类(class,第4-5章内容)定义操作符

在算数表达式中(例如 2 + 3 × 5),乘除法的优先级高于加减法,因此优先计算乘除法( 3 × 5),但可以通过圆括号来改变求值顺序(例如 (2 + 3) × 5)操作符的优先级,与算术中加减乘除的优先级类似。

算术操作符

操作符含义示例
+1 + 2 //3
-4 - 3 //1
-expr符号取反-x
*3 * 5 //15
/除,结果是一个浮点数7 / 2 //3.5
~/整除,除法的整数部分,结果是一个整数7 ~/ 2 //3
%模,除法的余数部分,结果是一个整数或浮点数7 % 2 //1

Dart支持自增(++)与自减(--)一元操作符。表达式x++ 相当于x = x+1x-- 相当于x = x-1。 这两个操作符位于操作数之前或之后均可,但有区别。

// ex151.dart
void main() {
  var x = 3, y = 3;
  print('${x++} $x'); // Output: 3 4
  print('${++y} $y'); // Output: 4 4
}

表达式x++ 的值为x,先返回x,再将x自增1;而表达式++x的值为x+1,也就是先将x自增1,再返回x。 我们用同样的方式去理解x----x

关系操作符

关系操作符用于布尔表达式(其结果为truefalse)。

操作符含义示例
==等于5 == 5
!=不等于6 != 7
>大于8 > 7
<小于6 < 9
>=大于等于4 >= 3
<=小于等于3 <= 4

关于== 操作符:

  1. 如果 xynull,则只有当它们都是 nullx==y 才返回 true;如果只有一个为 null,则x==y 返回 false
  2. x == y本质上是调用 x== 操作符方法(以y作为参数),然后返回其结果, 类似于 x.equals(y)
  3. 极少情况下,你需要知道xy是否引用了同一个对象,使用identical(x,y)

NOTE: 方法、对象的概念将在第4章介绍。

类型测试操作符

asisis! 操作符用于在运行时检查类型。

操作符含义示例
as类型转换(该符号也用于import语句中给包取别名)obj as String
is如果对象具有指定类型返回trueobj is double
is!如果对象不具有指定类型返回trueobj is! num

赋值操作符

首先来看 =??= 这两个赋值操作符。

操作符含义示例
=赋值x = 123;
??=仅当变量为null才对其赋值 x ??= 234;

复合赋值操作符组合了一个操作和赋值,以 += 为例, x += y 就等价于 x = x + y。假设 op 是一个操作符,那么 a op= b 等价于 a = a op b。以下是复合赋值操作符:

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

逻辑操作符

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

操作符含义示例
!expr逻辑非,逻辑取反!x
||逻辑或(OR)x == 3 || x ==4
&&逻辑与(AND) x >=1 && y < 6

下面是一个稍微复杂的例子:

if (ok && (x < 5 || y > 6)) {
    print('good');
}

位操作符

Dart可以对数字的每一比特位(bit)进行操作。

操作符含义示例
&与(AND) 1 & 2 // 0
|或(OR)1 | 2 // 3
^异或(XOR)2 ^ 3 //1
~expr按位补码(0变为1,1变为0)~1 // -2
<<左移位1 << 10 // 1024
>>右移位1024 >> 9 // 2
>>>无符号右移-1 >>> 61 // 7

条件表达式

先来看下面的代码片段:

  int n;
  if (x < 3) {
    n = 3;
  } else {
    n = x;
  }

上面计算n 的代码,用条件表达式就简化为:

  var n = x < 3 ? 3 : x;

条件表达式 condition ? expr1 : expr2 ,有 ?:2个操作符,?之前为一个表示条件的布尔表达式condition;如果condition的值为true,该表达式的值就取expr1的值,否则取 expr2的值。

级联符号

Dart的级联符号(..?..)用于对同一个对象进行一连窜操作。下面的代码片段:

  var sb = StringBuffer();
  sb.write('Hello ');
  sb.write('Dart');
  sb.write('!');
  print(sb);

用级联符号就简化为:

  print(
    StringBuffer()
      ..write('Hello ')
      ..write('Dart')
      ..write('!'),
  );

?....类似,只不过用于nullable对象的第一个操作。

  sb2
    ?..write('Hello ')
    ..write('Dart')
    ..write('!');

上面代码中的sb2如果是null,就不会执行任何的级联操作。

其他操作符

下面的操作符已在前面章节介绍过,不再赘述。

操作符含义示例
()函数调用doIt()
[]下标访问todoList[0]
?[]有条件下标访问fruits?[0]
.成员访问point.x
?.有条件成员访问point?.x
!非空断言point!
...展开一个集合(collection),插入另一个集合(collection)...myList
...?...类似,适用于nullable对象....?anotherList

注: () 也用于改变表达式的运算优先级。

操作符优先级

下表中的操作符按优先级从高到低排依次列,第一行中的优先级最高,第二行次之,以此类推; 同一行里的操作符具有相同的优先级。

描述操作符
一元后缀expr++   expr--   ()   []   ?[]   .   ?.   !
一元前缀-expr   !expr   ~expr   ++expr   --expr   await expr
乘除*   /   %   ~/
加减+   -
移位<<   >>   >>>
位与 AND&
位异或 XOR^
位或 OR|
关系及类型测试>=   >   <=   <   as   is   is!
相等==   !=
逻辑与 AND&&
逻辑或 OR||
if-null??
条件expr1 ? expr2 : expr3
级联..   ?..
赋值=   *=   /=   +=   -=   &=   ^=
展开...   ...?

参考资料

注释

代码中的注释帮助人们阅读代码,文档注释还用于生成API(应用编程接口)文档。Dart 支持三种注释:

单行注释和多行注释会被编译器忽略,而文档注释可用于Dart的文档生成工具,dart doc

单行注释

单行注释以//开头,从//到这一行的末尾,都会被Dart编译器忽略。

  // This line will be ignored by Dart compiler
  print('Hello');

多行注释

多行注释以 /* 开头,以*/ 结尾。

  /*
  This whole paragraph will be ignored by Dart compiler.
  Greetings to Dart.
  */
  print("Hello Dart!");

文档注释

文档注释以///开头(单行文档注释),或以/**开始以*/结束(多行文档注释)。在连续的行上使用 /// 与多行文档注释具有相同的效果。

// ex162.dart

/// A student class encapsulates student information,
/// including id, name and birthdate.
/// 
/// This class is immutable.
class Student {
  final int id;
  final String name;
  final DateTime birthdate;
  const Student(this.id, this.name, this.birthdate);
}

参考资料

再谈 Null Safety:从语法到运行时

在 Dart 中,?(可空性)和 !(空断言)不仅仅是语法糖,它们深深植根于 Dart 的健全类型系统(Sound Static Type System)。理解它们的底层机制,需要从编译时分析与运行时检查两个维度入手。

Dart 类型系统

Dart 的 Null Safety 并不是一个孤立的功能,它本质上是对类型层级图的一次重构。 在深入探讨 Null Safety 机制之前,我们先对 Dart 健全类型系统(Sound Type System) 做一个系统性的梳理。

类型层级图

graph TD
    %% 顶层类型
    Object_Q["Object? (Top Type)"]
    
    %% 中间层与可空分支
    Null["Null"]
    Object["Object (Non-nullable)"]
    
    %% 基础类型
    num["num"]
    int["int"]
    double["double"]
    String["String"]
    bool["bool"]
    List["List&lt;T&gt;"]
    Function["Function"]
    Other["..."]
    
    %% 特殊类型
    dynamic["dynamic (Static Check Bypass)"]
    void["void (No return value)"]
    Never["Never (Bottom Type)"]

    %% 关系连接:从顶层向下
    Object_Q --- Object
    Object_Q --- dynamic
    Object_Q --- void
    Object_Q --- Null
    
    %% Object 的直接子类
    Object --- num
    Object --- String
    Object --- bool
    Object --- List
    Object --- Function
    Object --- Other
    
    %% num 的具体实现
    num --- int
    num --- double
    
    %% Never 的关系 (作为所有类型的最底层)
    int --- Never
    double --- Never
    String --- Never
    bool --- Never
    List --- Never
    Function --- Never
    Other --- Never
    Null --- Never

    %% 样式美化
    style Object_Q fill:#f9f,stroke:#333,stroke-width:2px
    style Never fill:#ff9,stroke:#333,stroke-width:2px
    style dynamic fill:#eee,stroke:#333,stroke-dasharray: 5 5
    style Object fill:#e1f5fe,stroke:#01579b

Object? 位于类型层级图的顶层,Never位于底层,第二层是 ObjectdynamicvoidNull。在 Dart 类型规范中,Object?dynamicvoid 通常被并列视为顶层类型(Top Types),但由于 Object?是所有类型的超集(包含 Null),将其放在最顶层作为“根节点”是非常直观且正确的理解。

在 Dart 的类型系统中,Never 是一个非常特殊且强大的存在。它被称为底层类型 (Bottom Type),意味着它是所有其他类型的子类,而它本身没有任何实例。 Never 最常见的用途是标识“永不返回”的函数:

Never fail(String message) {
  throw Exception(message); // 函数在此中断,不会返回任何值
}

?: 可空类型的底层机制

在 Dart 运行时,intint? 实际上被视为两种不同的类型。那么int? 到底位于类型层级图的哪里? 实际上 int? 则是 intNull 类型的联合类型(Union Type),只能位于类型层级图的第二层。我们可以通过下表来展示这种“联合类型”的层级位置,你会发现,int? 实际上扮演了一个中转站的角色:

graph TD
    %% 顶层
    Object_Q["Object?"]

    %% 第二层:可空包装层 (Union Types)
    subgraph Nullable_Layer [第二层:可空联合类型]
        direction LR
        int_Q["int? (int ∪ Null)"]
        String_Q["String? (String ∪ Null)"]
        bool_Q["bool? (bool ∪ Null)"]
    end

    %% 第三层:基础不可空类型
    Object["Object"]
    Null["Null"]
    int["int"]
    String["String"]
    bool["bool"]

    %% 关系连接
    Object_Q --- int_Q
    Object_Q --- String_Q
    Object_Q --- bool_Q
    Object_Q --- Object

    %% 联合类型的构成
    int_Q --- int
    int_Q --- Null
    
    String_Q --- String
    String_Q --- Null
    
    bool_Q --- bool
    bool_Q --- Null

    %% Object 的子类
    Object --- int
    Object --- String
    Object --- bool

    %% 底部
    Never["Never"]
    int --- Never
    String --- Never
    bool --- Never
    Null --- Never

    style int_Q fill:#e1f5fe,stroke:#01579b,stroke-dasharray: 5 5
    style Nullable_Layer fill:#f9f9f9,stroke:#ccc

当你尝试将 int?类型的值 赋给 int 时,Dart 编译器会报错。这是因为在层级图中,你是在尝试向上转型(从联合类型转到更具体的子类型),这是不安全的。

当我们声明一个非空类型变量 int x,我们就向编译器做出了永久性承诺。在 AOT 编译阶段,由于确定 x 永远不会是 null,编译器可以大胆地移除所有原本需要插入的 if (x == null) 检查指令。这种 “非空检查消减(Null Check Elimination)” 让生成的机器码更加紧凑、执行路径更加单一。这也是为什么 Null Safety 能提升程序执行性能

! 的运行时代价:隐式分支检查

! 操作符(非空断言)实际上是静态检查的“逃生舱”,但它是有代价的。每当我们使用 x!,Dart 编译器会在该位置强制注入一段检查逻辑。在 CPU 执行层面,! 触发的是一次条件跳转(Conditional Branch),如果 x 恰好为 null,它会跳转到异常处理路径;虽然单次执行极快,但在高性能计算(如我们会学到的 WorkerPool)中频繁使用 ! 会干扰 CPU 的分支预测。

在编写高性能代码时,我们应尽量通过架构设计(如构造函数初始化)确保变量非空,从而利用编译器的检查消减特性提示性能,而不是依赖运行时频繁的 ! 检查。

Type Promotion(类型提升)

Type Promotion 是 Dart 编译器的一项智能特性。它指的是:当编译器能够通过逻辑流分析(Flow Analysis)确定一个变量在特定范围内绝对属于某种更窄的类型时,它会自动临时改变该变量的类型。简单来说,就是编译器帮你省去了手动进行强制类型转换(如使用 as 关键字)的麻烦。 例如:

void process(Object data) {
  if (data is List) {
    // 💡 data 从 Object 提升到了 List
    print(data.length); // 现在可以直接访问 List 的属性
  }
}

Null Safety 提升

这是 Dart 开发者每天都会遇到的。当一个变量是“可空类型”(如 String?),我们对其进行非空检查后,在该分支内它会被自动提升为“非空类型”(String)。

void printLength(String? text) {
  // print(text.length); // 报错:Property 'length' cannot be accessed on 'String?'

  if (text != null) {
    // 💡 在这个 if 块内,text 被自动提升为 String (非空)
    print(text.length); // 正常运行
  }
}

为什么 if (obj.field != null) 之后依然报错?

这是开发者最常遇到的困惑:为什么 if (obj.field != null) 之后,直接使用 obj.field 依然报错?

原因在于内存竞争与 Getter 不确定性:

  • Isolate 并发: Dart 虽然是单线程模型,多 Isolate 之间虽然不共享内存,但在某些底层交互或 FFI(Foreign function interface) 场景下,数据的状态可能改变。

  • Getter 覆盖:底层 VM 无法保证 obj.field 是一块静态内存。它可能是一个 get 函数,每次调用都返回不同的值。

解决方案:使用局部变量快照(Shadowing)。

  final val = obj.field; // 将状态“锁定”在当前栈帧
  if (val != null) {
    print(val.length);
  }

Reference

if 与 switch 语句

程序的控制结构包括:

  • 顺序(顺序执行)
  • 分支(ifswitch
  • 循环(forwhile

分支语句包括 ifswitch语句。

if

下例中,如果 isSunny的值为true,就执行打印,否则什么也不做。

// ex211.dart
import 'dart:math';

void main() {
  final rand = Random();
  final isSunny = rand.nextInt(10) % 2 == 0;
  if (isSunny) {
    print("Let's go to the park");
  }
}  
flowchart TD
    B{条件}-->|true| C(代码块)
    C --> E([End])
    B -->|false| E

if 有一个可选的else分支。

  if (isSunny) {
    print("Let's go to the park");
  } else {
    print("It's not sunny");
  }
flowchart TD
    B{条件}-->|true| C(主分支)
    C --> E([End])
    B -->|false| D(else分支)
    D --> E

还可以使用 else if,像下面这样。

  if (isSunny) {
    print("Let's go to the park");
  } else if (isRaining) {
    print("Let's bring our raincoats");
  } else {
    print("Let's stay home");
  }

从语法上讲,上例中最后的else分支也是可选的。

if-case

从 Dart 3.0 开始, if 语句支持 case 子句,case后面接一个模式(pattern,第3章内容)。

// ex212.dart
void main() {
  var dat = [1, 2]; 
  if (dat case [_, _]) { // 1
    print('matched'); // 2
  } else {
    print('not matched'); // 2a
  }

  if (dat case [int x, int y]) { // 3
    print('x=$x y=$y'); // 4
  }
}

  1. 使用 if-case 检查 dat 是否匹配一个包含2个元素的列表;
  2. 如果匹配,就打印字符串 "matched",否则打印"not matched" (2a);
  3. 使用 if-case 将 dat与列表进行匹配,并捕获列表中的元素。

switch

如果需要匹配多个case,应使用switch 语句,而非一个又一个的else if

flowchart TD
    A{switch}-->|case1| B(case1)
    A --> |case2| C(case2)
    A --> |case...| D(case...)
    A --> |default| E(default分支)
    B --> Z([End])
    C --> Z
    D --> Z
    E --> Z
// ex213.dart
void main() {
  var status = 'done';
  switch (status) {
    case 'done':
      print('done');
    case 'ongoing':
      print('ongoing');
    case 'pending':
      print('pending');
    default: // case _:
      print('unknown status: $status');
  }
}

如果其他case都不匹配,则执行default case的代码,也就是 defaultcase _ 标记的代码 。default case 要放在最后。

fallthrough 与 标签(lablel)

空case(empty case)是指不做任何处理的case。默认情况下空case会fall through到下一个case,使得cases可以共享代码块。如果想让空case fall through到其他的case(非下一个case),使用标签(label)和 continue来实现。

// ex214.dart
void main() {
  var status = 'canceled';
  switch (status) {
    case 'canceled': // 1 empty case
    case 'done': // 1a
      print('done');
    todo: // 2
    case 'todo':
      print('todo');
    case 'pending':
      print('pending');
      continue todo; // 3
    case 'ongoing':
      print('ongoing');
    case _:
      print('unknown status: $status');
  }
}
  1. 这是一个空case( canceled ),它没有对应的代码块,fall through 到下一个case(done ), 这两个case共享了代码块(print('done')) ; 此外这两个case也可以写成
    case 'canceled' || 'done':
      print('done');
  1. 这里定义了一个标签 todo
  2. 在执行完上一行代码后(print('pending')),跳转至 todo 标签所在位置继续执行( print('todo'))。

如果不想让空case fall through, 使用 break 即可。

    case 'canceled':
      break;

switch 表达式

switch表达式将返回一个值,这个值来自于匹配的case的表达式。先来看一个普通的switch语句:

// ex215.dart
void main() {
  var x = 1.0, y = 2.0;
  var op = '+';
  double r;
  switch (op) {
    case '+':
      r = x + y;
    case '-':
      r = x - y;
    case '*':
      r = x * y;
    case '/':
      r = x / y;
    case '%':
      r = x % y;
    case _:
      throw 'unknown op: $op';
  }
  print('$x$op$y=$r');
}

switch表达式改写:

// ex216.dart
void main() {
  var x = 1.0, y = 2.0;
  var op = '+';
  var r = switch (op) {
    '+' => x + y,
    '-' => x - y,
    '*' => x * y,
    '/' => x / y,
    '%' => x % y,
    _ => throw 'unknown op: $op',
  };
  print('$x$op$y=$r');
}

在语法方面,与switch 语句相比,switch表达式:

  1. case不以 case 关键字开头;
  2. case主体是一个单一的表达式,而不是一系列的语句;
  3. 每个case都必须有一个主体;对于空case,没有隐式的 fallthrough;
  4. case模式与其主体使用 => 而不是 : 分隔;
  5. 各个case之间用 , 分隔(允许在结尾添加可选的 , );
  6. default case 只能用 _, 无法用default

详尽性检查

详尽性检查也叫完备性检查。下面这段代码将引起编译错误:

//ex217.dart
void main() {
  bool? win;
  var score = switch (win) {
    true => 2,
    false => 0,
  };
  // Error: The type 'bool?' is not exhaustively matched by the switch cases since it doesn't match 'null'.
  // Try adding a wildcard pattern or cases that match 'null'.
  //   var score = switch (win) {
  //                       ^
}

对于一个 boo?类型的变量,它的值是穷举的,即 truefalsenull,Dart编译器将对这类可穷举类型的变量进行详尽性检查。上例的编译错误告诉我们缺少了对 null case 的处理。常见的可穷举类型还有bool、枚举(Enum)和密封类(sealed class)

下例演示了针对枚举的详尽性检查。

// ex218.dart
void main() {
  var x = 1.0, y = 2.0;
  var op = Op.plus;
  var r = switch (op) {
    Op.plus => x + y,
    // Op.minus => x - y,
  };
  // Error: The type 'Op' is not exhaustively matched by the switch cases since it doesn't match 'Op.minus'.
  //  - 'Op' is from 'bin/ch02/ex218.dart'.
  // Try adding a wildcard pattern or cases that match 'Op.minus'.
  //   var r = switch (op) {
  //                   ^
}

enum Op { plus, minus }

下例演示了针对密封类的详尽性检查。密封类的子类数量是有限的,编译器将对其进行switch详尽性检查。

// ex219.dart
void main() {
  Op op = Plus(1.0, 2.0);
  var r = switch (op) {
    Plus(:double x, :double y) => x + y,
    // Minus(:double x, :double y) => x - y,
  };
  // Error: The type 'Op' is not exhaustively matched by the switch cases since it doesn't match 'Minus()'.
  //  - 'Op' is from 'bin/ch02/ex219.dart'.
  // Try adding a wildcard pattern or cases that match 'Minus()'.
  //   var r = switch (op) {
  //                   ^
}

sealed class Op {
  final double x;
  final double y;

  Op(this.x, this.y);
}

class Plus extends Op {
  Plus(super.x, super.y);
}

class Minus extends Op {
  Minus(super.x, super.y);
}

第4-5章将详细讨论类的相关概念。

守护子句

守护子句(guard clause)是可选的,它出现在case子句之后,使用关键字 when。守护子句可以出现在 if-case 以及 switch 语句和表达式中。

// ex21a.dart
void main() {
  var year = 2025;
  bool showAges = true;
  switch (year) {
    case >= 2000 when showAges && year < 2010:
      print('2000s');
    case >= 2010 when showAges && year < 2020:
      print('2010s');
    case >= 2020 when showAges && year < 2030:
      print('2020s');
  }

  var dat = [3, 4];
  if (dat case [int x, int y] when x < y) {
    print('x=$x y=$y');
  }
}

参考资料

for 与 while 循环

控制结构里的循环指的是重复执行一段代码,直到循环条件不再满足,或使用break关键字跳出循环。Dart控制结构里的循环包括:

  • 经典 for 循环
  • for-in 循环
  • while 循环
  • do-while循环

for 循环

下面这段代码使用经典的for循环打印数字0到9。

// ex221.dart
void main() {
  for (var i = 0; i < 10; i++) {
    print('$i');
  }
}

经典for循环的语法:

  for (初始化列表; 循环条件; 后置操作) {
    循环体
  }
flowchart TD
    A([初始化列表]) --> B{循环条件}
    B -->|true| C(循环体)
    C -->|continue| D(后置操作)
    D --> B
    B ---->|false| E([End])
    C ---->|break| E

后置操作即执行完循环体之后的操作。初始化列表、 循环条件、后置操作,这三部分用分号(;)分割,且每一部分都可以省略(但必须保留分号)。如果循环体只有一条语句,包围循环体的那对花括号({}) 可以省略,但不建议这么做。

下面这段代码将打印九九乘法表:

// ex222.dart
import 'dart:io';

import 'package:sprintf/sprintf.dart';

void main() {
  for (var i = 1; i < 10; i++) {
    for (var j = 1; j < 10; j++) {
      stdout.write(sprintf('%2d ', [i * j]));
    }
    stdout.write('\n');
  }
}

下面这段代码将数字10分解为2个非负整数之和:

// ex223.dart
void main() {
  for (var i = 0, j = 10; i <= j; i++, j--) {
    print('$i + $j = 10');
  }
  // Output:
  // 0 + 10 = 10
  // 1 + 9 = 10
  // 2 + 8 = 10
  // 3 + 7 = 10
  // 4 + 6 = 10
  // 5 + 5 = 10
}

下面这段代码的功能一样,但省略了循环条件 i <= j, 改用break语句来跳出循环:

// ex224.dart
void main() {
  for (var i = 0, j = 10; ; i++, j--) {
    if (i > j) break;
    print('$i + $j = 10');
  }
}

for-in

使用经典的for语句打印字母A-E:

// ex225.dart
import 'dart:io';

void main() {
  var letters = ['A', 'B', 'C', 'D', 'E'];
  for (var i = 0; i < letters.length; i++) {
    stdout.write('${letters[i]} '); // Output: A B C D E
  }
}

使用 for-in语句改写上述代码,更为简洁:

// ex226.dart
import 'dart:io';

void main() {
  var letters = ['A', 'B', 'C', 'D', 'E'];
  for (var letter in letters) {
    stdout.write('$letter '); // Output: A B C D E
  }
}

whiledo-while

while 循环在循环体之前检查循环条件,而do-while则是在循环提之后检查。

while 循环:

flowchart TD
    B{循环条件}-->|true| C(循环体)
    C -->|continue| B
    C -->|break|E
    B ---->|false| E([End])

do-while 循环:

flowchart TD
    C(循环体) -->|continue| B{循环条件}
    B -->|true| C
    C -->|break|E
    B ---->|false| E([End])

下面这段代码先打印数字0到9,再打印数字10到1(递减)。

//ex127.dart
void main() {
  // print 0,1,...,9
  var x = 0;
  while (x < 10) {
    print(x++);
  }

  // print 10,9,...,1
  do {
    print(x--);
  } while (x > 0);
}

breakcontinue

break用于跳出当前循环,continue用于跳转至下一次迭代。下面这段代码使用for循环,打印1到9之间的奇数。

// ex228.dart
void main() {
  for (var x = 1; ; x++) {
    if (x == 10) break;
    if (x % 2 == 0) continue;
    print(x);
  }
}

标签

breakcontinue 仅对所在的(内层)循环起作用。如果要跳出(break)外层循环,或跳转至(continue)外层循环的下一次迭代,须借助标签。标签是一个标识符,后跟冒号(labelName:);将标签放在语句之前以创建带标签的语句。

// ex229.dart

import 'dart:io';

void main() {
  outerLoop:
  for (var i = 0; i < 10; i++, print('')) {
    for (var j = 0; j < 10; j++) {
      if (j == 4) continue outerLoop;
      stdout.write(' ' * (i > 0 ? 1 : 2));
      stdout.write(10 * i + j);
    }
  }
}

此例中的 continue outerLoop; 就相当于 break

练习

  • 分别使用 whiledo-while 改写 ex228.dart
  • 示例 ex229 中,如果将 continue outerLoop; 改为 break outerLoop; 程序的输出是什么?

断言

斷言(Assertion)是一个很有用的开发工具。下面的代码在调试模式(debug mode)下运行,将产生一个断言错误(AssertionError)。

// ex231.dart
void main() {
  var str = '';
  assert(str.isNotEmpty, "str cannot be empty");
  // do something else
}

assert 接收2个参数,第一个参数必须是布尔表达式, 第二个参数是可选的,乃错误提示信息;在调试模式下,当第一个参数的值为false时,将出现断言错误。

断言仅在调试模式(debug mode)下起作用。对于发布模式(release mode),除非在编译时显示启用了断言, 所有的assert 语句都将被忽略,从而不会干预程序的执行流程。在Dart的相关命令中(例如 dart rundart compile exe),通过--enable-asserts 参数启用断言,如

dart run --enable-asserts bin/ch02/ex231.dart

函数 (Function)

本章节将介绍函数的基本概念,位置参数、命名参数、函数类型、匿名函数等内容,将在后续章节陆续介绍。我们已经多次见过main函数,它是程序的入口。抽象地说,函数对输入数据(输入参数列表)进行处理,然后输出其处理结果(返回值)。

flowchart LR
   a(( )) -->|输入参数| B(处理(函数体))
   B --> |返回值|c(( ))

先来看一个示例程序,计算平面上两点之间的欧式距离:

\( \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2} \)

//ex241.dart
import 'dart:math';

typedef Point = ({double x, double y}); // 1

void main() {
  var a = (x: 1, y: 2), b = (x: 3, y: 4); // 2
  var distance = sqrt(
    (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y),
  ); // 3
  print('$a -> $b: $distance'); // 3a

  var c = (x: 6, y: 5); //4
  distance = sqrt((a.x - c.x) * (a.x - c.x) + (a.y - c.y) * (a.y - c.y)); //5
  print('$a -> $c: $distance'); // 5a
}

  1. 使用typedef创建一个类型别名Point,它代表了二维平面里的一个点;
  2. 声明并初始化2个Pointab;
  3. 计算ab间的欧式距离,随后打印 (3a);
  4. 声明并初始化Point c;
  5. 计算ac间的欧式距离,随后打印 (5a);

将计算欧式距离的代码,抽取成一个函数 distance,然后改写上例:

//ex242.dart
import 'dart:math';

typedef Point = ({double x, double y}); 

void main() {
  Point a = (x: 1, y: 2), b = (x: 3, y: 4);
  print('$a -> $b: ${distance(a, b)}'); // 2

  Point c = (x: 6, y: 5);
  print('$a -> $c: ${distance(a, c)}'); // 3
}

double distance(Point a, Point b) { // 1
  final dx = a.x - b.x;
  final dy = a.y - b.y;
  return sqrt(dx * dx + dy * dy);
}
  1. 定义函数distance,它计算平面上两点间的欧式距离;
  2. 调用distance计算ab间的欧式距离;
  3. 调用distance计算ac间的欧式距离。

重构(改写)后的程序,功能不变,但更为简洁易读,消除了重复代码(计算欧式距离),并可对该代码块(distance 函数)进行单元测试(第8章内容)。

命名函数

通常,创建一个命名函数的语法为:

返回值的类型 函数名称(输入参数列表){
   函数体
}

对于上述 distance函数:

  • 返回值的类型: double;
  • 函数名称:distance
  • 输入参数列表:Point a, Point b
    • Point a 表示输入参数 a的数据类型为Point;
  • 花括号({})内的代码为函数体;
  • 使用return返回函数的返回值。

如果函数没有返回值,用void表示:

void printMe(String me) {
  print(me);
  // return;
}  

像此例中的return;,它位于函数体的最后一行,通常予以省略。

下面两点通常不被鼓励,除非你有充分的理由,因为它让Dart的静态类型分析失效:

  • 命名函数的返回值的类型,如果不写出来,就表示它是 dynamic (而不是void);
  • 类似地,命名函数的输入参数的类型,如果不写明,就代表dynamic

返回值

如果没有指定返回值,则语句 return null; 会被隐式附加到函数体中,函数将返回null

如果一个函数需要返回多个值,通常返回一个记录(Record)类型。

// ex243.dart
(bool ok, String) foo() {
  // Do something
  return (true, 'done');
}

arrow 语法

如果函数体只有一个表达式,可以使用arrow(箭头)语法来简化。例如

int increase(int value) {
  return value + 1;
}

使用arrow语法简化为:

int increase(int value) => value + 1;

getter 与 setter

getter 与 settter 是一种特殊的函数,分别使用 getset 关键字定义 。

// ex244.dart
var _foo = 1;

int get foo => _foo;

set foo(int value) {
  if (value > 0) {
    _foo = value;
  }
}

void main() {
  print('foo=$foo'); // Output: foo=1
  foo = 2;
  foo = -1;
  print('foo=$foo'); // Output: foo=2
}

此例中的foo在使用者看来,就像一个变量,但foo setter 给私有变量 _foo提供了额外的保护,它只将正数赋值给_foo

call 方法

函数类型有个call方法,其签名(参数列表与返回值)与函数本身一样,调用call的行为与调用函数本身也一样。

// ex245.dart
int square(int n) => n * n;

void main() {
  print(square(10)); // Output: 100
  square.call(10); // Output: 100
}

main函数

程序的入口main函数,返回void,有一个可选的参数,其类型为List<String>,用于命令行程序中。

// ex246.dart
void main(List<String> args) {
  for (var arg in args) {
    print(arg);
  }
}

此程序打印命令行参数,一个参数一行。使用dart run运行它:

$ dart run bin/ch02/ex244.dart hello dart
hello
dart

NOTE: 可使用 args 库来定义和解析命令行参数。

外部函数(external function)

外部函数的函数体(实现)与函数声明是分离的。外部函数的实现可以来自另一个Dart库,更常见的是,来自其它编程语言(例如C)。使用 external 声明一个外部函数,例如 Object类中的 toString() 方法:

  external String toString();

小测验

下例中的printMe2的返回类型是什么?实际上它返回了什么?参数me的类型又是什么?

printMe2(me) {
  print(me);
}

参考资料

函数参数

函数参数有位置参数命名参数之分,另外还有可选参数

位置参数

上一节中的示例函数,只使用了位置参数,这些参数在参数列表中的顺序至关重要。

// ex251.dart
void main(List<String> args) {
  print(power(2, 5)); // 2 Output: 32
  print(power(5, 2)); // 3 Output: 25
}

int power(int base, int exponent) { // 1
  int result = 1;
  for (int i = 0; i < exponent; i++) {
    result *= base;
  }
  return result;
}
  1. 创建函数power, 返回 baseexponent次方;
  2. 打印 2 的 5 次方(32);
  3. 打印 5 的 2 次方(25)。

可见在调用power函数时,必须注意参数的顺序。

命名参数

命名参数,顾名思义,就是有名字的参数。下面使用命名参数来创建power函数:

// ex252.dart
void main() {
  print(power(base: 2, exponent: 5)); // 2 Output: 32
  print(power(exponent: 2, base: 5)); // 3 Output: 25
}

power({required int base, required int exponent}) { // 1
  // ... (omited for brevity)
}

语法方面,

  • 命名参数被一对花括号({})包围;
  • required 关键字,表示函数的调用方必须传递该参数;
  • 调用函数时,采用参数名:参数值的形式(如base: 2),给命名参数传值 。

可选参数

对于命名参数,无required修饰时,表示是可选的。

无论是位置参数还是命名参数:

  • nullable(可空)参数,可选参数的默认值是null;
  • non-nullable (不可空)参数,必须指定一个默认值,才能成为可选参数;
  • 在参数名后使用 = 来指定默认值。
// ex253.dart
void main() {
  print(increase(5)); // Output: 6
  print(increase(5, increment: 2)); // Output: 7

  print(descrease(5)); // Output: 4
  print(descrease(5, 2)); // Output: 3
}

int increase(int value, {int increment = 1}) => value + increment;

int descrease(int value, [int? decrement]) => value - (decrement ?? 1);

可选的位置参数(可以是多个),必须放在参数列表的最后,放在一对中括号里([ ])。

混合使用位置参数与命名参数

位置参数和命名参数可以混合使用,如上例中的int increase(int value, {int increment = 1}),但混合使用时:

  • 位置参数必须在参数列表的开头(即先定义位置参数);
  • 此时不能定义可选的位置参数。

函数类型、匿名函数与闭包

在Dart里,函数像普通变量一样,可作为参数传递,或从一个函数返回,这便是所谓的函数乃一等公民(first-class object)。在Dart里可以定义匿名函数 ,也就是没有名称的函数,这在实际应用中极为方便。闭包(Closure)是与之相关的另一个重要的概念,详见后文。

函数类型

首先通过一个示例,来看一看什么是函数类型(funtion type)。

// ex261.dart
void main() {
  int increase(int value, {int increment = 1}) => value + increment; // 3
  var increaseFunc = increase; // 4

  print(addFunc); // 5
  print(increaseFunc); // 5a

  //Output:
  //Closure: (int, int) => int from Function 'add': static.
  //Closure: (int, {int increment}) => int
}

int add(int a, int b) => a + b; // 1

int Function(int a, int b) addFunc = add; // 2

int Function(int, int) addFunc2 = add; // 2a

var addFunc3 = add; // 2b
  1. 声明函数add;
  2. 声明变量addFunc 并赋值为add,它的类型是一个函数类型 int Function(int a, int b) , 可见函数类型的写法与声明一个函数类似,去掉函数体,然后将函数名称换成关键字Function即可,同时可省略位置参数的名称(2a);
  3. main函数的内部,声明函数increase;
  4. 声明变量increaseFunc 并赋值为increase;
  5. 打印addFuncincreaseFunc,注意观察其输出。

increase 函数定义在main函数的内部,这便是嵌套函数的概念。Closure: (int, int) => int from Function 'add': static.,这表明addFunc的值是一个闭包(Closure),并且它来自全局(static)的add函数。这里的全局讲的是作用域(Lexical Scope)。

使用 typedef

使用 typedef 关键字给函数类型取一个别名:

typedef AddFunction = int Function(int a, int b);
AddFunction addFunc4 = add;

typedef VoidCallback = void Function();

typedef 也用于给数据类型取别名:

typedef IntList = List<int>;

词法范围(Lexical Scope)

从语法上看,一对花括号({})便定义了一个词法范围,即作用域。作用域嵌套而成,外层作用域内的变量对内层可见,反之不然。下面是来自官方文档的示例:

// ex262.dart
bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

匿名函数

main print这些是命名(具名)函数。我们也可以创建没有名称的函数,称之为匿名函数。

匿名函数经常作为其他函数的参数,它与命名函数类似,特别之处在于:

  • 没有函数名称;
  • 也无需声明返回值的类型;
  • 参数的类型是可选的。

语法格式:

(参数列表) {
  函数体
}

下面的示例程序分析一句英文,然后打印其中的单词及其长度。

// ex263.dart
void main() {
  var sentence = 'Its quaint events were hammered out'; // 1
  var stats = sentence.split(' ').map((word) => (word, word.length)); // 2
  stats.forEach(print); // 3
  // Output:
  // (Its, 3)
  // (quaint, 6)
  // (events, 6)
  // (were, 4)
  // (hammered, 8)
  // (out, 3)
}
  1. sentence是一句英文;
  2. sentence.split(' ') 使用空格符(' ')分割 sentence,得到一个单词(word)的列表; 然后将每个单词映射(map)为一个记录,内容为单词及其长度((word, word.length)); 这一句结束时将得到统计结果stats,它是一个 Iterable 对象(可将Iterable简单理解为一个更为泛化的列表,通过特定的方法访问其元素);
  3. 使用Iterable.forEach 方法,打印stats 中的每个元素,这里将print函数作为参数传入forEach

第2句中的(word) => (word, word.length) 是一个匿名函数,它作为参数传入map函数。

闭包(Closure)

闭包是一个容易让人迷糊的概念。简言之,闭包是一个函数(或函数值、函数对象),很多时候它就是一个匿名函数。闭包这个概念之所以会诞生,是因为它的实用性与特殊性:

闭包即使脱离了定义它的作用域(原始作用域),依然可以访问原始作用域内的变量。

换言之,当一个函数引用了其外部作用域的变量时,它就形成了一个闭包。闭包就像一个带有‘记忆背包’的函数,即使离开了出生的家,包里依然装着它从家里带出来的变量。

下面这个示例来自官方文档

// ex264.dart
Function makeAdder(int addBy) {// 1
  return (int i) => addBy + i; // funcA
}

void main() {
  var add2 = makeAdder(2); // 2
  var add4 = makeAdder(4); // 2a

  assert(add2(3) == 5); // 3
  assert(add4(3) == 7); // 3a
}
  1. makeAdder是一个函数,它返回一个匿名函数 (int i) => addBy + i; ,为方便描述,称之为funcA;
  2. 分别调用 makeAdder(2)makeAdder(4) 得到函数 add2add4;
  3. 断言 add2(3)等于5, 断言 add4(3)等于7。

add2add4 都记住了makeAdder被调用时的addBymakeAdder的参数)。从数学的角度来看funcA:

\( funcA( i ) = addBy + i \)

这个函数里的自变量是iaddBy是一个常量。 add2 = makeAdder(2), 相当于指定常量 addBy = 2add4类似。add2add4 等都是闭包。

深度解析

内存中的“生存转移”

当 Dart 编译器发现一个函数引用了外部变量时,它会做一件特殊的事情:将这些变量从“栈”提升到“堆”中。

  • 普通局部变量:存储在栈上,函数退出,内存立即回收。

  • 被捕获的变量:存储在一个特殊的“环境对象”(Context Object)中,存放在堆上。只要匿名函数(闭包)还被引用,这个环境对象就不会被 GC(垃圾回收)。

生命周期延长现象

闭包使得变量的生命周期(Lifetime)脱离了它原本的作用域(Scope)。

“捕获的是变量,而不是值”

闭包捕获的是对变量引用的持有,而不是该变量在捕获那一刻的快照。这意味着:

  • 共享状态:如果有多个闭包捕获了同一个变量,它们共享同一个状态。
  • 副作用:如果在外部修改了该变量,闭包内看到的值也会变。
// ex265.dart
void main() {
  var x = 10;
  void showX() => print(x);

  x = 20;
  showX(); // Output: 20
}

最佳实践

匿名函数及其形成的闭包是极其强大的工具,但若误用很容易导致内存泄漏、内存占用过高或GC抖动(短时间内频繁触发GC)。 编程时要注意避免 “过度捕获”陷阱长生命周期持有

Dart 的闭包捕获是基于整个作用域对象的,而不仅仅是捕获那个被用到的变量。

class MyWidget {
  String name = "Widget";
  List<int> massiveData = List.generate(1000000, (i) => i);

  void doSomething() {
    // 这个匿名函数虽然只用了 name,但由于它捕获了隐式的 'this'
    // 导致整个 MyWidget 实例(包括 massiveData)都无法被释放
    button.onTap = () => print(name); 
  }
}

实践中,我们要及时“切断”闭包与外部作用域变量的持有关系。

a. 及时置空

如果一个长生命周期的对象持有了一个回调,在不需要时将其设为 null

button.onTap = null; // 切断匿名函数及其捕获作用域的链接

b. 局部变量转换(避免捕获 this

void doSomething() {
  final nameSnapshot = this.name; // 将属性存为局部变量
  button.onTap = () => print(nameSnapshot); // 只捕获字符串,不捕获 'this' 对象
}

c. 生命周期对齐

在 Flutter 开发中,闭包的寿命应严格受限于 Widget 的生命周期。利用 dispose 是切断关系的黄金时机。

  • 监听器清理:使用 controller.removeListener(myListener)
  • 流订阅清理:调用 streamSubscription.cancel()

d. 避免在异步间隙中持有(Async Gap)

await 之后的闭包操作要格外小心,因为 await 会挂起当前函数,此时所有的局部变量都会保持存活。

Future<void> process(MassiveObject huge) async {
  await someLongRunningTask(); // 挂起期间,huge 对象一直无法被回收
  print(huge.id);
}

如果 huge 对象很大,且 await 之后只需要它的一个 id,请先提取 id,然后让 huge 脱离作用域或设为 null

练习

修改示例程序ex263,将统计结果按单词的长度降序排序。提示:使用List.sort方法。

递归

如果一个函数直接或间接调用它自己,我们便称之为递归函数。之所以讨论它,是因为递归是一种解决问题的有效方法,也是一种分治的思想,它针对特定的问题,这类问题在结构上可以进一步分解为相似的子问题。例如阶乘函数:

\( n! = (n-1)!*n \)

下面的代码计算斐波那契(Fibonacci)数列:

\( fib(n) = \begin{cases} n, \ n=0 \ 或\ 1 \\ fib(n-1) + fib(n-2), \ n >1 \end{cases} \)

// ex271.dart
void main() {
  var n = 20;
  print('fib($n) = ${fib(n).$2}'); // Output: fib(20) = 6765
}

/// fib function returns (fib(n - 1), fib(n))
(int, int) fib(int n) {
  if (n <= 1) return (0, 1);
  var (a, b) = fib(n - 1);
  return (b, a + b);
}

又如使用辗转相除法,计算两个整数的最大公约数(最大公因数):

\( gcd(a, b) = gcd(b, a\%b) \)

// ex271.dart
void main() {
  var a = 234, b = 678;
  print('gcd($a, $b) = ${gcd(a, b)}'); // Output: gcd(234, 678) = 6
}

/// Greatest common divisor
int gcd(int a, int b) {
  if (b == 0) return a;
  return gcd(b, a % b);
}

NOTE: 辗转相除法

  1. 用较大数除以较小数,得到余数;
  2. 如果余数为0,则除数即为最大公约数(算法终止),否则转下一步;
  3. 用余数代替原来的除数,用原来的除数代替原来的被除数,转第1步。

递归与循环

有时,递归函数可以用循环来改写,例如:

/// Greatest common divisor (loop version)
int gcd2(int a, int b) {
  while (b != 0) {
    (a, b) = (b, a % b);
  }
  return a;
}

gcd2gcd函数的功能一样,只不过使用了循环而非递归来实现。

异常处理

Dart代码可以抛出(throw)和捕获(catch)异常(Exception)。异常代表了程序的某种错误,也就是一些意想不到的事情。引发异常的代码分支及其上下文,即测试中的负向路径。

Dart的函数或方法不会声明它将抛出哪些异常; Dart也不强制要求捕获异常。如果异常发生了却不被处理,抛出异常的 isolate 将被挂起,且通常会被终止。第7章将详细讨论 isolate,这里将其视为一种内存独立的线程(如 main 函数所在的 main isolate)即可。

抛出异常 (throw

Dart程序可以抛出(throw)什么?答案是除了null外的任何东西。

// ex281.dart
void main() {
  throw 'something bad happened';
  // Unhandled exception:
  // something bad happened
}

此例使用throw 关键字,抛出了一个字符串。

ExceptionError

Dart提供了 ExceptionError 类及相关子类(第4章将介绍类),官方建议我们在产品代码中抛出 ExceptionError 的实现类。例如下面这些类实现了Exception

  • AnalysisException
  • DeferredLoadException
  • FileSystemException
  • FormatException
  • IOException
  • IsolateSpawnException
  • PathException
  • RemoteException
  • TimeoutException

我们也可以创建自己的异常类,并实现(implementsExceptionError

// ex282.dart
void main() {
  throw FooException();
}

class FooException implements Exception {}

ExceptionError 的区别:

  • Exception旨在向外传递一个失败信息(例如超时异常 TimeoutException),以便通过编程的方式解决;它应该包含有用的信息,且希望被捕获。

  • Error代表了程序员引起的错误(例如断言错误 AssertionError), 它理应被避免,且通常暗示了一个bug,我们不应捕获它。

捕获异常

捕获指定类型的异常

使用 try...on...catch 语法捕获异常。将可能抛出异常的代码放在try{}代码块中,然后使用on关键字捕获特定类型的异常。

// ex283.dart
import 'dart:io';

void main() {
  var str = 'dart';
  try {
    int.parse(str);
  } on FormatException {
    stderr.writeln('FormatException: Cannot parse "$str" as an integer.');
  }
}

此例试图将字符串"dart"转换为一个整数,引发了 FormatException,随后被捕获。

捕获异常信息

使用catch关键字捕获异常信息(即异常类型的一个实例)。

// ex284.dart
void main() {
  try {
    int.parse('dart');
  } on FormatException catch (ex) {
    print(ex.message); // Output: Invalid radix-10 number
  } 
}

catch() 还有另一个可选参数,表示异常堆栈。

// ex285.dart
void main() {
  try {
    int.parse('dart');
  } on FormatException catch (e, s) {
    print(e);
    print(s);
  }
}

重新抛出异常

使用 rethrow关键字重新抛出异常,且保留原来的堆栈信息。

// ex286.dart
void main() {
  try {
    parseInt('dart');
  } catch (_, s) {// 2
    print(s); // 2a
  }
}

int parseInt(String str) {
  try {
    return int.parse(str);
  } on FormatException catch (_, s) {
    print(s); // 1a
    rethrow; // 1
  }
}
  1. 使用rethow关键字将捕获到的FormatException再次抛出;
  2. 这里仅用了catch (没有使用on指定异常类型),表示捕获所有类型的异常。

捕获多种类型的异常

使用多个catch子句来分别处理不同类型的异常。

//ex267.dart
void main() {
  print(safeDivide('8', '2'));
  print(safeDivide('8', '0'));
  print(safeDivide('hello', 'dart'));
}

(String? err, int) safeDivide(String a, String b) {
  try {
    return (null, int.parse(a) ~/ int.parse(b));
  } on FormatException catch (e) {
    return ('FormatException: ${e.source}', 0);
  } on UnsupportedError catch (e) { // IntegerDivisionByZeroException
    return ('UnsupportedError: ${e.message}', 0);
  } on Exception catch (e) {
    return ('unknown exception: $e', 0);
  }
}

finally

无论try 代码块内是否发生了异常, finally子句都将被执行。finally子句通常用来关闭资源(如打开的系统文件)。

// ex288.dart
void main() {
  try {
    print(2 ~/ 0);
  } on UnsupportedError {
    print('oops');
  } finally {
    print('done');
  }
  // Output:
  // oops
  // done
}

参考资料

模式

Dart 3.0引入了一个非常强大的功能,模式(Pattern)。一般来说,依据模式的形状及其上下文, 模式可以匹配(match)解构(destructure)一个值,匹配与解构可同时进行。

匹配

模式匹配可以检查一个值是否:

  • 具有特定的形状;
  • 是特定常数;
  • 等于某个值;
  • 有特定的数据类型。

这么说还是有点抽象,我们来看下面的示例。

//ex311.dart
void main() {
  const numbers = [1, 2, 3]; // 1

  if (numbers case [_, _, _]) { // 2
    print('numbers is a list which has 3 elements.');
  }

  if (numbers case [1, 2, 3]) { // 3
    print('numbers is exactly [1, 2, 3].');
  }

  const point = (x: 3, y: 4); // 4
  
  if (point case (x: _, y: _)) { // 5
    print("point is a record with named fields x and y.");
  }
}

  1. 声明了一个const列表numbers;
  2. 检查numbers是否是一个具有2个元素的列表;
  3. 检查numbers是否就等于 [1, 2, 3];
  4. 声明一个记录point;
  5. 检查point是否是一个具有命名字段 xy 的记录(record)。

解构

模式解构提供了一种便捷的声明式语法,可以将一个值分解成各部件(parts),并将部分或全部的部件,绑定至局部变量。

//ex312.dart
void main() {
  const numbers = [1, 2, 3]; 

  if (numbers case [var a, var b, _]) { // 1
    print('a=$a b=$b'); 
  }

  if (numbers case [var a, ...var rest]) { // 2
    print('The first element is $a, and the rest is $rest'); 
  }

  const point = (x: 3, y: 4); 

  if (point case (x: var a, :var y)) { // 3
    print("a=$a y=$y");
  }
}
  1. 匹配numbers并绑定前2个元素至变量ab;
  2. 匹配numbers并绑定第1元素至变量a,绑定剩余的元素至变量rest,注意var rest之前有3个点(....);
  3. 匹配point,并将其x字段绑定至变量ay字段绑定至变量y;这里的:var yy:var y的简便写法。

下面是与类相关的模式匹配的一个示例:

// ex313.dart
void main() {
  const point = Point(3, 4); // 4
  if (point case Point(:var x, :var y, :var z)) { // 5
    print("x=$x, y=$y, z=$z"); 
  }
}

class Point { 
  const Point(this.x, this.y); // 1

  final int x; // 2
  final int y; // 2a
  int get z => x + y; // 3
}
  1. 这是类Point的构造函数;
  2. x yPoint 的两个公开的字段;
  3. z 是 getter 方法,由xy计算而成,形式上当成字段来用;
  4. 声明一个变量point,它Point类的一个实例;
  5. 解构point,并将其部件分别绑定值变量 xyz

解构不但可以绑定字段,还可以绑定函数/方法(在类中定义的函数),也就是为什么在描述解构时,采用了部件(而非字段)这个词。

// ex314.dart
void main() {
  const rect = Rectangle(5, 6);
  if (rect case Rectangle(:var area)) {
    print("${area(.1)} m²");
  }
}

class Rectangle {
  const Rectangle(this.a, this.b);

  final double a;
  final double b;

  double area([double unit = 1]) => a * b * unit * unit;
}

参考资料

模式的使用场景

模式可用于下列地方:

局部变量声明和赋值

// ex321.dart
void main() {
  var (name, :id, [a, b, c]) = ('Tom', id: 1, [3, 4, 5]); // 1
  print('name=$name id=$id a=$a b=$b c=$c');
  // Output: name=Tom id=1 a=3 b=4 c=5

  (a, b) = (b, a); // 2
  print('a=$a b=$b'); // Output: a=4 b=3
}
  1. 这行代码展示了模式如何用于局部变量声明;
  2. 这里使用了模式的局部变量赋值,交换两个变量的值。

if-case 和 switch-case

每个case子句都包含了一个模式,无论是if-case 还是switch-case。

// ex322.dart
void main() {
  const data = ('Tom', id: 1, [3, 4, 5]);
  if (data case ('Tom' || 'Jerry', id: _, _)) { // 1
    print('Hey, my friend!');
  }

  switch (data) {
    case ('Tom', :var id, _):
      print('Hi, Tom! Your ID is $id.');
    case ('Jerry', :var id, _):
      print('Hi, Jerry! Your ID is $id.');
    default:
      print('Hi! Who are you?');
  }
}
  1. 这里使用了逻辑或(OR)操作符(||),让data.$1匹配多个值,这非常实用。

for 和 for-in 循环

// ex323.dart
void main() {
  var points = [Point(0, 1), Point(1, 2), Point(2, 3)]; // 2
  for (var Point(:x, :y) in points) { // 3
    print('x=$x y=$y');
  }
}

class Point { // 1
  const Point(this.x, this.y);
  final double x;
  final double y;
}
  1. 定义Point类,它包括xy两个字段;
  2. points是一个Point列表;
  3. 在for-in语句中解构points中的每个Point,将其部件绑定至局部变量xy

集合字面量(collection literals)中的控制流

集合包括List、Set、Map,将在第6章将详细讨论,这里仅以List(列表)为例,对模式的相关使用进行说明。

首先来看如何使用for语句创建一个列表,以及forif的嵌套使用。下面的程序将打印数字0到9中的偶数。

// ex324.dart
void main() {
  final nums = [for (var i = 0; i < 10; i++) i]; // 1
  var evenNums = [
    for (var num in nums) // 2
      if (num.isEven) num,
  ];
  print(evenNums); // Output: [0, 2, 4, 6, 8]
}
  1. 在集合字面量中使for语句,创建一个0到9的数字列表nums;
  2. nums进行过滤(if(num.isEven)),选出其中的偶数。

下例演示了如何在集合字面量的控制流中使用模式。

// ex325.dart
void main() {
  var points = [for (var x = 0; x < 10; x++) (x: x, y: 2 * x)];
  var points2 = [
    for (var (:x, :y) in points)
      if (x.isEven) (x: x, y: y),
  ];
  print(points2);
}

模式类型

类似于操作符优先级,模式也有优先级,也可以通过圆括号(最高优先级)来改变求值顺序。模式优先级由低到高依次为:

集合型(记录 record、列表 List、映射map)与对象模式包含了其他数据,作为外部模式优先求值。

“逻辑或”模式

这个模式在上一章节已经出现过,如 if (data case ('Tom' || 'Jerry', id: _, _)) 中的 'Tom' || 'Jerry' ,其语法为:

subpattern1 || subpattern2

只要任何一个子模式匹配,”逻辑或“模式就匹配;子模式从左至右求值,直到匹配上了一个子模式,剩下的子模式将不被求值。

“逻辑与”模式

subpattern1 && subpattern2

当它的子模式都匹配, ”逻辑与“模式才算匹配;子模式从左至右求值,当遇到一个子模式不匹配时,剩下的子模式将不被求值。

// ex331.dart
void main() {
 var goods = (name: 'apple', price: 2.0, quality: 'good');
 switch (goods) {
   case (name: _, price: _, quality: 'good') &&
       (name: 'apple' || 'banana', :var price, quality: _):
     print('Good apple or banana for \$$price');
   default:
     print('Take a look at other fruits');
 }
}

此例中的 \$ 为转义用法,表示 $ 符号本身。

这个示例旨在说明“逻辑与”模式的用法,它实在可以简化为:

// ex332.dart
void main() {
 // ... (omit for brevity)
 switch (goods) {
   case (name: 'apple' || 'banana', :var price, quality: 'good'):
   // ... (omit for brevity)
 }
}

关系模式

关系模式使用关系运算符(==!=<><=>=)将一个值与常量进行比较。下例绘制分段函数:

\( f(x)= \begin{cases} 0 & x <0 \\ 2x & 0 \le x < 10 \\ 20 & 10 \le x < 20 \\ 3 * (x - 20) + 20 & x \ge 20 \\ \end{cases} \ ,\ x是整数 \)

// ex333.dart
import 'package:sprintf/sprintf.dart';

void main() {
  var xs = [for (var x = -5; x <= 30; x++) x];
  var ys = [for (var x in xs) fx(x)];
  // Draw the bar chart
  for (var i = 0; i < xs.length; i++) {
    print(sprintf('%2d | %s %d', [xs[i], bar(ys[i]), ys[i]]));
  }
}

int fx(int x) => switch (x) {
  < 0 => 0,
  >= 0 && < 10 => 2 * x,
  >= 10 && < 20 => 20,
  _ => 3 * (x - 20) + 20,
};

String bar(int y) => '▩' * y;

该程序将打印一个水平方向的条形图:

-5 |  0
-4 |  0
-3 |  0
-2 |  0
-1 |  0
 0 |  0
 1 | ▩▩ 2
 2 | ▩▩▩▩ 4
 3 | ▩▩▩▩▩▩ 6
 4 | ▩▩▩▩▩▩▩▩ 8
 5 | ▩▩▩▩▩▩▩▩▩▩ 10
 6 | ▩▩▩▩▩▩▩▩▩▩▩▩ 12
 7 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 14
 8 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 16
 9 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 18
10 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
11 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
12 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
13 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
14 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
15 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
16 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
17 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
18 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
19 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
20 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 20
21 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 23
22 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 26
23 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 29
24 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 32
25 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 35
26 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 38
27 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 41
28 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 44
29 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 47
30 | ▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩▩ 50

一元后缀模式

一元后缀模式包括 Cast(类型转换)、Null-check(空检查)、Null-assert(空断言)。

Cast模式使得在解构过程中插入类型转换,但如果类型转换失败,程序将抛出异常。

// ex344.dart
void main() {
  (Object, num) dat = ('Alice', 100);
  var (name as String, score as int) = dat;
  assert(name == 'Alice');
  assert(score == 100);

  var (x as double, _) = dat;
  // Unhandled exception:
  // type 'String' is not a subtype of type 'double' in type cast
}

Null-check模式在匹配一个值时,首先检查那个值是否为null,如果是null就直接匹配失败,但不会抛出异常。

Null-assert与Null-check类似,但当所匹配的那个值为null时会抛出一个异常。

// ex334.dart
void main() {
  int? a;
  checkNull(a); // Output: check: a is null

  a = 10;
  checkNull(a); // Output: check: a is 10

  assertNull(a); // Output: assert: a is 10

  assertNull(null);
  // Unhandled exception:
  // Null check operator used on a null value
}

void checkNull(dynamic a) {
  switch (a) {
    case var x?:
      print('check: a is $x');
    default:
      print('check: a is null');
  }
}

void assertNull(dynamic a) {
  switch (a) {
    case var x!:
      print('assert: a is $x');
  }
}

其它主要模式

List(列表)模式

List模式非常实用,例如利用该模式将一个List分成“首个元素+中间的元素s+尾部元素”三部分。

// ex336.dart
void main() {
  const luckyNumbers = [5, 6, 8, 9];
  var [first, ...rest, last] = luckyNumbers;
  print('first=$first last=$last'); // Output: first=5 last=9
  print('rest=$rest'); // Output: rest=[6, 8]
}

此例中的...rest 表示剩余元素形成的List,称之为rest元素(Rest element)。List模式至多包含一个rest元素,这不难理解,因为如果有2个rest元素,就无法确定其部件(parts)的边界。

Record(记录)模式

我们已经多次见过Record模式(见 记录类型 一节),在仅举一例,不过多描述。

// ex337.dart
void main() {
  const point = ('A', x: 1, y: 2, z: 3);
  var (name, :x, :y, z: _) = point;
  print('$name: x=$x y=$y'); // Output: A: x=1 y=2
}

Map (映射)模式

Map由key-value pair(键值对)组成,通过key获取value。例如:

  var employee = {"id": 1, "name": "Tom", "post": "CEO", "salary": 2};
  var numbers = {1: "one", 2: "two", 3: "three"};
  assert(employee["name"] == "Tom");

第6章将进一步讨论Map,此处仅举例说明Map模式的运用。

// ex338.dart
void main() {
  var employee = {"id": 1, "name": "Tom", "post": "CEO", "salary": 2};
  var numbers = {1: "one", 2: "two", 3: "three"};

  var {"id": id, "salary": salaray} = employee;
  assert(id == 1);
  assert(salaray == 2);

  var {1: one, 2: two} = numbers;
  assert(one == "one");
  assert(two == "two");

  var {4: four} = numbers;
  // Unhandled exception:
  // Bad state: Pattern matching error
}

Map模式的两个特点:

  • 无需匹配整个Map,也就是说无需匹配所有的Key;
  • 尝试匹配一个不存在的Key,将抛出一个StateError 错误

Object(对象)模式

Object模式在上一节已有所介绍,下面是另一个示例。

// ex339.dart
void main() {
  const color = Color(50, 60, 80);
  var Color(:r, :g, :b) = color;
  print('r=$r g=$g b=$b'); // Output: r=50 g=60 b=80
}

class Color {
  const Color(this.r, this.g, this.b);
  final int r, g, b;
}

参考资料

模式应用案例

本文以示例的方式,介绍模式的几个经典应用案例。

交换两个变量的值

//ex341.dart
void main() {
  var (x, y) = (3, 4);
  (x, y) = (y, x);
  assert(x == 4 && y == 3);
}

解构Map里的 key-value

//ex342.dart
void main() {
  const employee = {"id": 1, "name": "Tom", "post": "CEO", "salary": 2};
  for (var MapEntry(:key, :value) in employee.entries) {
    print("$key: $value");
  }
  // Output:
  // id: 1
  // name: Tom
  // post: CEO
  // salary: 2
}

解构多个返回值

// ex343.dart
void main() {
  var (ok, result) = foo();
  print(ok ? result : 'oops!');
}

(bool ok, String) foo() {
  // ...
  return (true, 'Everything is OK');
}

解构类实例

Object(对象)模式

代数(Algebraic)数据类型

当满足如下条件时,可使用代数数据类型这一风格去写代码:

  • 有一个相关类型的家族(family)。
  • 有一个操作,需要针对每种类型执行特定的行为。
  • 希望将这些行为集中到一起,而不是分散到不同的类型中。

这是一个来自官方文档的示例。

// ex344.dart
import 'dart:math' as math;

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
  Square(length: var l) => l * l,
  Circle(radius: var r) => math.pi * r * r,
};

JSON/Map 校验

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,其应用极为广泛,尤其在互联网领域。JSON采用了独立于编程语言的文本格式。

// ex345.dart
import 'dart:convert';

void main() {
  const dat = '''{
    "id": 1,
    "name": "Learning Dart from Zero",
    "category": "Digital Book",
    "tags": ["dart", "programming", "learning"],
    "website": "https://techo.dev/dartpl/"
  }'''; // 1

  final product = jsonDecode(dat); // 2
  print(product.runtimeType); // Output: _Map<String, dynamic>

  if (product case { // 3
    "id": int _,
    "name": String name,
    "tags": [String tag1, String tag2, _],
    "website": String website,
  }) {
    print("$name($website), tagged by $tag1 and $tag2");
  }
}
  1. dat 是一个JSON字符串;
  2. 使用 jsonDecode 函数解析 dat,其结果存于product变量,此处的product是一个Map;
  3. 使用模式校验product,并绑定相关的部件(parts)。

参考资料

https://dart.dev/language/patterns#use-cases-for-patterns

封装与可见性

封装、继承与多态是面向对象编程(Object-oritented programming, OOP)的三大特性,而封装是其中最为基础与关键的特性。Go语言向我们证明了继承对于OOP是可选的,而多态通过接口来实现。这些基本概念将在后续章节陆续介绍,本章节重点说明封装。

封装

很多时候我们想要隐藏代码的实现细节,而对外暴露的只是API(应用编程接口),这便是针对接口(而非实现)编程,要实现这一点就要用到封装。接口用于定义API,而类就像一个容器主要用于封装。那么类究竟封装了什么?

// ex411.dart
class Dog {
  const Dog(this.name, this.age); // 3
  final String name; // 1
  final int age; // 1a

  void say() {  //2
    print('$name: Woof woof');
  }

  void eat() { //2a
    print("$name: I'd like to have some bones");
  }
}
  1. 此例中Dog类封装了其属性(即字段 nameage),以及
  2. 行为,即方法 say()eat();方法(method)即在类中定义的函数;
  3. 这是Dog的构造函数。

类的实例化与成员

下面这行代码使用构造函数,创建(实例化)了Dog类的一个实例(instance):

  var dog = Dog('Spots', 6);

我们说dogDog类的一个实例或对象。

NOTE: 类是对象的模板,对象是类的实例。

类的成员包括实例变量、实例方法、静态变量、静态方法。

Dog类中的nameage是实例变量,像这样引用:dog.name

Dog类中的say()eat()是实例方法,像下面这样调用:

  dog.say();

实例变量与实例方法是与类的实例关联(绑定)的,而静态(static)变量与静态方法则与类关联,例如:

class Dog {
  // .... (omit for brevity)
  static const ponyo = Dog('Ponyo', 5);
  static void ponyoSay() => ponyo.say();
}  

可见性

Dart 没有使用 public, protectedprivate 关键字来表达成员的可见性,而是使用特殊的标记即下划线(_)来表达private(私有)的概念。

class Dog {
  // .... (omit for brevity)
  final _secret = "I won't tell you this";
  void _tellSecret() => print(_secret);
}  

_secret字段与_tellSecret() 方法对于外部文件是不可见的,换言之,它只能在当前文件(定义它的文件)中使用。

Libraries(库)

Dart里的每个.dart文件(加上它的子文件,即parts)都是一个libary。使用import关键字导入libary中的代码。这种机制使得代码可以被模块化以及可以被复用。

file lib/hellodart.dart:

// lib/hellodart.dart
int calculate() { // 1
  return 6 * 7;
}

file bin/hellodart.dart:

// bin/hellodart.dart
import 'package:hellodart/hellodart.dart' as hellodart; // 2

// ch0101
void main(List<String> arguments) {
  print('Hello world: ${hellodart.calculate()}!'); // 3
}
  1. lib/hellodart.dart 中定义函数calculate();
  2. 这行代码使用关键字 import 导入 lib/hellodart.dart ,并使用 as 关键字给此library取名hellodart,将其中的标识符置于hellodart命名空间里;
  3. hellodart.calculate()表示调用hellodart library 中的calculate()函数。

NOTE:

  1. 第2行import一句中的 package:hellodart 表示 hellodart这个package(包),package的名称与 pubspec.yaml 文件中 name: hellodart 是一致的;回想一下,我们在第1章Hello Dart: 搭建开发环境一节中创建了helloword 这个package。
  2. 如果第2行代码不使用 as 关键字给 hellodart.dart 取名,表示将此libary导入全局命名空间,就直接使用calculate()来调用该函数。

我们在 lib/hellodart.dart 中定义另一个函数:

int _calculate() => 7 * 8;

然后在其它文件中(如 bin/hellodart.dart)调用它,将引起编译错误:

bin/hellodart.dart:6:35: Error: Method not found: '_calculate'.
  print('Hello world: ${hellodart._calculate()}!'); // 3
                                  ^^^^^^^^^^

构造函数

构造函数(或构造器,Constructor),是用来创建类实例(对象)的特殊函数。如上一节中

var dog = Dog('Spots', 6);

通过调用Dog类的构造函数,得到Dog类的一个实例 dog

Dart 实现了多种类型的构造函数:

这些函数总体上可以分为 生成式构造函数 与 工厂构造函数 两大类。它们在书写形式上无需指定返回类型,因为它们返回的一定是类的实例。

在本文的最后还将讨论构造函数的函数体

在讨论具体的构造函数之前,先来看一下实例变量的初始化

实例变量的初始化

在声明时初始化

在声明类的实例变量时,可对其进行初始化。例如

// ex421.dart
class Cat {
  var name = "Kitty";
  var age = 1;
}

实例变量 nameage 都有初始值,因此使用 var 关键字来声明,让Dart自动推断其类型。

初始化形参

Dart的初始化形参极大的简化了构造函数的书写。

// ex422.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age);
}

此处的 this 关键字表示当前实例,或则说 this 乃当前实例的引用。

初始化列表

上述示例代码(ex422)等价于:

// ex423.dart
class Cat {
  String name;
  int age;

  Cat(String name, int age) : name = name, age = age; // 1
}
  1. 冒号后面的 name = name, age = age 即初始化列表。

NOTE: 初始化列表中的 name = name,等号(=)左边是实例变量name,右边是构造函数的参数name

当然对于如此简单的初始化操作,使用初始化形参数更为简洁。

实战中经常要对构造函数的参数进行简单校验,比如要求 name 的长度至少为3:

// ex424.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age) : assert(name.length >= 3); // 1
}
  1. 这里在初始化列表里使用 assert对参数name的长度进行了校验。

有时候实例变量的初始化需要一个简单的计算,可以参考下例:

// ex425.dart
class Rectangle { // 1
  final double width;
  final double height;
  final double perimeter;

  Rectangle(this.width, this.height)
    : perimeter = _computePerimeter(width, height); // 3

  // 2
  static double _computePerimeter(double w, double h) => 2 * (w + h);
}
  1. Rectangle类代表了一个矩形;
  2. 定义一个静态的辅助方法_computePerimeter,用以计算矩形的周长;
  3. 在初始化列表中调用_computePerimeter来初始化实例变量 perimeter

生成式构造函数

生成式构造函数(Generative constructors),负责创建实例并初始化实例变量,是创建新实例的常见方式。上文已经演示了一个简单的生成式构造函数:

// ex422.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age);
}

默认构造函数

如何没有显示定义一个生成式构造函数,Dart将自动创建一个不具名的无参构造函数。

// ex426.dart
class Cat {
  var name = "Kitty";
  var age = 1;

  // Auto-created default constructor
  // Cat();

  @override
  String toString() {
    return 'Cat($name,$age)';
  }
}

void main() {
  print(Cat());// Output: Cat(Kitty,1)
}

命名构造函数

命名构造函数用于实现多个构造函数,并且命名构造函数的名称提供了额外的信息,以帮助用户使用(理解代码的意图)。

// ex427.dart
class Point { // 1
  final double x;
  final double y;

  Point(this.x, this.y); // 2

  Point.origin() : x = 0, y = 0; // 3

  @override
  String toString() {
    return '($x, $y)';
  }
}

void main() {
  print(Point(2, 3)); // Output: (2.0, 3.0)
  print(Point.origin()); // Output: (0.0, 0.0)
}
  1. Point 类代表了平面坐标系中的一个点;
  2. 使用初始化形参定义了一个构造函数;
  3. 定义了一个命名构造函数 Point.origin(),它使用初始化列表将实例变量xy的值都设置为0。

常量构造函数

如果要创建编译时( compile-time)常量对象,就需要用到常量构造函数(Constant constructors)。

// ex428.dart
class Point {
  final double x;
  final double y;

  const Point(this.x, this.y); // 1
}
  1. 使用 const 关键字定义常量构造函数。

NOTE: 常量构造函数所在类的实例变量必须为声明为 final

常量构造函数所创建的对象一定是常量吗?

void main() {
  var p1 = const Point(1, 2); // 2
  var p2 = Point(1, 2); // 3
  print(p1 == p2); // 4 Output: false

  const p3 = Point(1, 2); // 5
  print(p1 == p3); // 6 Output: true
}
  1. 在常量构造函数 Point(1, 2) 前使用关键字 const 创建Point类的一个实例p1;
  2. p2 是 Point类的另一个实例,但实例化时没有使用const关键字;
  3. 比较 p1p2 是否相等,结果为 false,可见它们引用了不同的实例;
  4. p3p2类似,但用了 const (而非var)关键字来声明;
  5. 比较 p1p3 是否相等,结果为 true,可见它们引用了同一个实例,该实例是一个常量;而p2对应的实例则不是常量。

从语法角度来看:

  1. var p2 = Point(1, 2); 等价于 var p2 = new Point(1, 2);, 这里的关键字 new 通常予以省略;
  2. const p3 = Point(1, 2); 这里的 const关键字开启了一个常量上下文,因此这句代码就相当于 const p3 = const Point(1, 2); ,而第二个const通常予以省略。

重定向构造函数

重定向构造函数没有函数体,它使用 this 关键字重定向到类中的另一个生成式构造函数。

// ex429.dart
class Point {
  double x, y;

  Point(this.x, this.y); // 1

  Point.x(double x) : this(x, 0); // 2
}
  1. 使用初始化形参定一个生成式构造函数;
  2. Point.x(double) 是一个重定向构造函数,它重定向至构造函数 Point(this.x, this.y)(语法上使用this关键字);这行代码的效果为:
  Point.x(this.x) : y = 0; // 2

工厂构造函数

设计模式中有一个创建型模式叫做工厂方法(也称为虚拟构造函数),它专门用来创建类的实例。Dart的工厂构造函数与设计模式里的工厂方法有点类似,但在结构上有所不同。工厂构造函数是一种返回类实例的特殊函数,它必须借助生成式构造函数,才能创建实例。下面按使用场景举例说明。

缓存实例

工厂构造函数用于缓存实例的一个经典示例是Logger(日志记录器)。

// ex42a.dart
class Logger {
  final String name;

  Logger._(this.name); // 1

  static final _cache = <String, Logger>{}; // 2

  factory Logger(String name) { // 3
    return _cache.putIfAbsent(name, () => Logger._(name));
  }

  factory Logger.main() => Logger("main"); // 4

  void log(String message) { // 5
    print('${DateTime.now()} $name: $message');
  }
}

void main() {
  assert(Logger("main") == Logger.main()); // 6
  Logger.main().log('This message is from main function'); // 7
}
  1. Logger._(this.name) 是一个私有的构造函数,对外部文本不可见;
  2. _cache是一个Map,用于缓存 Logger实例;
  3. Logger(String)是一个工厂构造函数,以关键字factory修饰,其内部操作为:如果name参数对应的Logger 实例已经在_cache中,就直接返回该实例,否则创建一个新的Logger实例存放于_cache,然后返回它;
  4. Logger.main() 是命名的工厂构造函数;
  5. log(String) 是用于打印日志的实例方法;
  6. 断言 Logger("main")Logger.main()返回同一个Logger实例;
  7. 调用 Logger.main()log方法打印一行日志。

创建系列对象

有时需要对一系列对象的创建过程进行集中管理,尤其是那些受保护的类。

// ex42b.dart
abstract class Toy {
  factory Toy(String name) {
    return switch (name) {
      'car' => _Car(),
      'truck' => _Truck(),
      'airplane' => _Airplane(),
      _ => throw 'unknown toy: $name',
    };
  }
}

class _Car implements Toy {}

class _Truck implements Toy {}

class _Airplane implements Toy {}

void main() {
  print(Toy('car'));
}

此例中的Toy是一个抽象类,它定义了一个工厂构造函数,该函数根据参数name来创建对应的实例。Toy的具体类(_Car_Truck_Airplane)是私有的(受保护的),随着时间的推移,它们的内部实现可能被改变,但不会影响用户代码。抽象类与接口等概念将在下一章介绍。

命名工厂构造函数与静态方法

实际上,命名工厂构造函数可以用返回类实例的静态方法进行改写,可以说二者是等价的。示例 ex42aLogger.main 方法可以改写为:

  static Logger main() => Logger("main"); // 4

构造函数的函数体

重定向构造函数没有函数体,其它生成式构造函数的函数体是可选的。示例 ex424 可以改写为:

// ex42c.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age) {
    assert(name.length >= 3);
  }
}

但一点需要注意,没有late关键字修饰的non-nullable实例变量的初始化,必须在 初始化形参 或 初始化列表中完成,而不可在函数体中完成。

工厂构造函数是一种返回类实例的特殊函数,它一定有函数体。

参考资料

延迟初始化(再谈late

late 关键字的本意是延迟变量的初始化。

// ex431.dart

// int x;
// Error:
// The non-nullable variable 'x' must be initialized.

late int x; // 1

void main() {
  x = 1; //2
  print(x); // Output: 1
}
  1. 声明int类型的变量 x 并用 late 关键字修饰它,告诉编译器暂时不对x进行初始化;如果去掉这里的 late将引起编译错误 The non-nullable variable 'x' must be initialized
  2. 在使用 x之前对其赋值。

什么时候使用 late

下面按应用场景对late的使用举例说明。

需要使用 non-nullable 变量,但在声明时又无法初始化(比如依赖另一个实例变量/方法 )。

// ex432.dart
class A {
  int x;
  late int y = x + 1;
  A(this.x);

  @override
  String toString() => '($x, $y)';
}

void main() {
  print(A(2)); // Output: (2, 3)
}

此例中变量 y 由变量 x计算而成,用late修饰再方便不过了。

变量的初始化是个费时操作,希望延迟初始化它。

// ex433.dart
import 'dart:math';

class RandAvg {
  int n;
  late double avg;

  RandAvg(this.n);

  static final _rand = Random();

  void init() {
    var s = 0.0;
    for (var i = 1; i <= n; i++) {
      s += _rand.nextDouble();
    }
    avg = s / n;
  }
}

void main() {
  var a = RandAvg(1000_000);
  a.init();
  print(a.avg);
}

此例中的 avgn 个随机浮点数(取值0-1)的算术平均数,其计算过程由 init() 函数完成。

变量仅在程序的部分地方用到,没用到的地方无需初始化。

// ex434.dart
import 'dart:io';

const kDebug = true;

class Logger {
  final String name;
  late String _debugInfo;

  Logger(this.name);

  void ensureDebugInfo() {
    if (kDebug) {
      _debugInfo =
          'OS=${Platform.operatingSystem} CPU cores=${Platform.numberOfProcessors}';
    }
  }

  void log(String message) {
    if (kDebug) {
      print(_debugInfo);
    }
    print('${DateTime.now()} $name: $message');
  }
}

void main() {
  var logger = Logger('main');
  logger.ensureDebugInfo();
  logger.log('Hi Dart');
}

仅当 kDebug 取值 true 时, Logger类的_debugInfo 才被 log 方法用到, 也才被初始化(由ensureDebugInfo方法完成)。

联合使用 late final

示例 ex433 中的实例变量 avg,在初始化之后便不再改变,这样的变量使用 late final 修饰更为准确,可以有效防止二次计算。

// ex435.dart
class RandAvg {
  int n;
  late final double avg;

  RandAvg(this.n);
}

最佳实践

late 在不同生命周期下的表现

全局变量 / 静态变量 (Static/Top-level)

这是 late 最能发挥威力的地方。Dart 的全局变量本身就是延迟初始化的。使用 late 可以让这种延迟语义更加明确。它避免了在 App 启动瞬间(main 函数执行前)加载大量非必要资源,从而缩短启动白屏时间。

类成员变量 (Instance Fields)

在对象实例化的生命周期内,late 会稍微增加对象的内存占用(因为需要存储初始化状态位)。性能权衡: 如果对象频繁创建且变量经常不被使用,late 是优化的;如果变量几乎总是会被立即使用,那么普通的 final 构造函数赋值性能更优,因为编译器可以进行更多的内联优化。

局部变量 (Local Variables)

Dart 编译器(尤其是 AOT 模式)通常能通过静态流分析(Definite Assignment Analysis)确认局部变量在使用前是否已赋值。在这种情况下,编译器会移除运行时的状态检查,性能等同于普通变量。

避免滥用 late

过度使用 late 的风险包括代码运行风险代码膨胀。一方面,late可能将本该在编译期发现的错误推迟到了运行期(抛出 Error),此即运行风险。另一方面, late 是用微小的运行时开销换取了开发的灵活性,每一处 late 访问在汇编后的代码中都会多出一层逻辑判断(代码膨胀)。在绝大多数业务逻辑中,这种开销是无感的;但在每秒数百万次的计算中(高性能场景),它确不可被忽视。在高性能要求的场景下,如果能通过构造函数初始化列表(Initializer List)解决的问题,优先使用普通非空变量。

性能与安全的权衡

使用方式性能影响建议场景
late T v = ...节省启动开销,首访微量开销昂贵的资源加载、单例模式
late T v;每次访问微量检查开销必须在 initState 或构造函数体赋值的非空变量
普通 final性能最优(编译器优化空间最大)能在声明处或构造函数初始化列表确定的值

方法(Method)

方法即类中定义的函数,有实例方法与静态方法之分,这在类的实例化与成员一节中已介绍过。 本文将介绍 Getter 与 Setter 方法抽象方法。 另外有一种特殊的方法是操作符,将在下一节中介绍。

Getter 与 Setter 方法

第2章讨论过 getter与 setter函数,getter 与 setter 方法与之类似,只不过方法定义在类中。

// ex411.dart
class Foo {
  Foo(this._bar);

  int _bar; // 1

  int get bar => _bar; // 2

  set bar(int value) => _bar = value; //3
}

void main() {
  var foo = Foo(100);
  print(foo.bar); // 4 Output: 100

  foo.bar = 200; // 5
  print(foo.bar); // Output: 200
}
  1. _bar是一个私有的成员变量;
  2. 定义一个 getter (get bar),它返回实例变量_bar的值;
  3. 定义一个 setter (set bar),通过它来修改_bar的值;
  4. 调用 get bar 这个getter方法 ;
  5. 调用 set bar 这个setter方法 。

语法上看,此例中的 bar 跟一个实例变量无异,实在没有必要专门定义 getter 和 setter ,即此例中Foo类等价于:

// ex412.dart
class Foo {
  Foo(this.bar);
  int bar; // 1
}

从方法的角度看,类中的公开字段(不以下划线开头)隐式地定义了对应的 getter 和 setter

何时使用 getter

使用 getter 的一个典型场景是计算型属性。这里的属性(property)即拥有getter的实例变量。

// ex413.dart
class Foo {
  Foo(this.bar);

  int bar; // 1

  int get bar2 => bar * bar;
}

void main() {
  var foo = Foo(1);
  assert(foo.bar2 == 1);

  foo.bar = 2;
  assert(foo.bar2 == 4);
}

此例中的 bar2bar 计算而成,像数学函数里的因变量。

下面来看一下如何使用 getter 定义只读属性。试想有一个代办任务类(Task),它的当前状态(status),只允许在 Task 内部修改,但允许类的外部代码访问,也就是说 status 对外是只读的。

// ex444.dart
class Task {
  Task(this._status) : assert(_status >= 0);
  int _status;

  int get status => _status;
}

何时使用 setter

代码总是在一次次迭代(Iteration,敏捷开发术语,指一个小的开发周期,比如2周)中完善。为方面用户使用Task类,又同时让其 _status 始终得到良好的维护,下面为 _status 添加一个setter。

// ex445.dart
class Task {
  // ... (omitted for brevity)

  set status(int value) {
    if (value > 0) {
      _status = value;
    }
  }
}

此例中的 set status 在 修改_status 前进行了校验,这便是有条件地更新。还可以在setter中添加一些附加操作,例如打印日志。完整的示例如下:

// ex446.dart
class Task {
  Task(this._status) : assert(_status >= 0);
  int _status;
  
  int get status => _status;

  set status(int value) {
    if (value > 0) {
      final old = _status;
      _status = value;
      print('status changed from $old to $_status');
    }
  }
}

void main() {
  var t = Task(0);
  t.status = 1; // Output: status changed from 0 to 1
}

有时两个或多个实例变量之间有一定的约束关系。比如指定一个长方形的周长,那么长+宽的值便是固定的(等于半周长)。

// ex447.dart
class Rect {
  Rect(this.perimeter, this.width);

  final int perimeter;
  int width;

  int get height => perimeter ~/ 2 - width;
  set height(int value) => width = perimeter ~/ 2 - value;
}

void main() {
  var rect = Rect(100, 10);
  assert(rect.width == 10);
  assert(rect.height == 40);

  rect.width = 20;
  assert(rect.width == 20);
  assert(rect.height == 30);

  rect.height = 20;
  assert(rect.width == 30);
  assert(rect.height == 20);
}

此例中Rect类的 heightperimeterwidth 计算而成,通过它的setter修改其值,实际上是在修改width的值 ,之所以为height定义setter仅仅是为了计算上或概念理解上的方便

抽象方法

抽象类可以拥有抽象方法。没有方法体(也就是没有具体的实现)的方法,称为抽象方法。 使用 abstract 关键字定义抽象类,例如:

// ex448.dart
abstract class Mammal {
   void breathe();
}

此例中的 Mammal 是一个抽象类,它的 breathe() 为抽象方法。

抽象类及抽象方法的常见应用:

  • 使用纯抽象类即接口(interface),定义API(应用编程接口);
  • 设计类层次,即类的分层设计;
  • 抽象基类提供某个算法的骨架实现,子类只需实现抽象方法或覆盖某些方法即可,即实现设计模式里的模板方法

下一章将更详细地讨论抽象类、抽象方法和接口等。

操作符(实例方法)

第1章已详细讨论过操作符,本文将从方法的角度再次讨论它。Dart多数的操作符,是拥有特殊名称的实例方法,这些名称是:

<	>	<=	>=	==	~
-	+	/	~/	*	%
|	ˆ	&	<<	>>>	>>
[]=	[]

但有些运算符,例如 !=,不在上述列表中,这些运算符不是实例方法,它们的行为是 Dart 内置的。

示例1:分数

下面以分数(Fraction)为例,来看看如何在类中定义操作符。

// ex451.dart
int gcd(int a, int b) { // 1
  while (b != 0) {
    (a, b) = (b, a % b);
  }
  return a;
}

class Fraction {
  Fraction(int num, int den) : assert(den != 0) { // 3
    final d = gcd(num, den);
    this.num = num ~/= d;
    this.den = den ~/= d;
  }

  late final int num; // 2 Numerator
  late final int den; // 2a Denominator

  Fraction operator +(Fraction f) => // 4
      Fraction(num * f.den + f.num * den, den * f.den);

  Fraction operator -(Fraction f) => // 4a
      Fraction(num * f.den - f.num * den, den * f.den);

  @override
  String toString() => den == 1 ? '$num' : '$num/$den'; // 5
}

void main() {
  var f1 = Fraction(3, 2); // 6
  var f2 = Fraction(2, 3);  // 6a
  assert("13/6" == (f1 + f2).toString()); // 7
  assert("5/6" == (f1 - f2).toString()); // 7a
}
  1. gcd 函数计算两个整数的最大公约数(最大公因数);
  2. num 代表分子,den 代表分母;
  3. Fraction 构造函数,自动进行约分操作;
  4. 使用关键字operator 加上 +/- 符号,定义分数的加/减法;分数加法公式为: \( \frac{b}{a} + \frac{d}{c} = \frac{bc+ad}{ac} \)
  5. Fraction的toString()方法,返回像 3/2(2分之3)这样的字符串;
  6. 声明分数变量 f1 (3/2)f2 (2/3);
  7. 分别计算 f1+f2f1-f2 并校验计算结果。

注意此例是如何使用 operator + 定义分数的加法的,可以将 operator + 整体想象成方法名 add,以方便理解语法。

注:为简洁起见,此例并未处理分子或分母是负数的情况。

示例2:num

Dart core中的 num 类定义的操作符:

part of dart.core;

sealed class num implements Comparable<num> {
  // ... (omitted for brevity)
  bool operator ==(Object other);
  num operator +(num other);
  num operator -(num other);
  num operator *(num other);
  num operator %(num other);
  double operator /(num other);
  int operator ~/(num other);
  num operator -();
  bool operator <(num other);
  bool operator <=(num other);
  bool operator >(num other);
  bool operator >=(num other);
  // ... (omitted for brevity)
}  

注:num 类中的操作符是抽象方法,意味着它们是由Dart VM(虚拟机)或 native code 实现的。

示例3:String

Dart core中的 String 类定义的操作符:

part of dart.core;

abstract final class String implements Comparable<String>, Pattern {
  // ... (omitted for brevity)
  String operator [](int index);
  bool operator ==(Object other);
  String operator +(String other);
  String operator *(int times);
  // ... (omitted for brevity)  
}

练习

请完善示例 ex451

  1. Fraction 支持分子或分母是负数;
  2. Fraction 添加乘(*)、除(/)操作符;
  3. main 函数数中,使用断言测试下新功能。

参考资料

callable 类

Dart的callable类的实例, 可以像函数一样被调用。听起来有点神秘,但一个示例即可揭开它的面纱。

// ex461.dart
class Greeter {
  String call(String name) => 'Hello, $name!'; // 1
}

void main() {
  var g = Greeter(); // 2
  print(g('World')); // 3 Output: Hello, World!
  print(g.call('Dart')); // 3a Hello, Dart!
}
  1. Greeter类中有一个名为 call 的实例方法;
  2. gGreeter的一个实例;
  3. g 当作函数来调用,实际上调用了实例方法 g.call

Greeter 定义了一个名为 call 实例方法,像这样的类即 callable 类,其实例为 callable 对象。换句话说, call 实例方法让一个类变得 callable。

除了call()实例方法外,callable类当然还能有其它的方法和字段。

// ex462.dart
class Greeter {
 String call(String name) => 'Hello, $name!'; // 1

 String hi(String name) => 'Hi, $name!'; // 2
}

void main() {
 var g = Greeter(); // 3
 print(g.hi('Auggie')); // 4 Output: Hi, Auggie!
}

此例中的callable类 Greeter 定义了一个普通的实例方法 hi,这样的方法也被称为命名函数

接下来看看callable类的应用场景。

有状态的函数

设想有一个函数,需要在多次调用之间维护其内部状态。比起使用全局变量或复杂的闭包,callable类显得更加简洁。

// ex463.dart
class Counter {
  int _count = 0; // 1
  int call() => ++_count; // 2
  void reset() => _count = 0; //3
}

void main() {
  final count = Counter();
  print(count()); // Output: 1
  print(count()); // Output: 2
  count.reset();
  print(count()); // Output: 1
}
  1. Counter类是一个计数器, 实例变量 _count 代表计数,是Counter的内部状态;
  2. call()方法每调用一次,_count 的值就加1并更新后的值;
  3. reset()方法将_count重置为0。

有状态的回调

事件处理函数或回调函数,有时需要管理与事件相关的状态,这是calllable类的另一个用武之地。

// ex464.dart
class Button {
  Function()? onClick; // 1
  void click() => onClick?.call(); // 2
}

class Callback {
  final String _context; // 3
  Callback(this._context); // 4
  void call() => print('$_context click'); // 5
}

void main() {
  var btn = Button();
  btn.onClick = Callback('Alice');
  btn.click(); // Output: Alice click

  btn.onClick = Callback('Bob');
  btn.click(); // Output: Bob click
}
  1. Button类表示图形界面上的按钮,当按钮被点击时,执行回调函数 onClick
  2. click 方法用来模拟按钮被按下,注意这里是如何调用 onClick 函数的(见call方法);
  3. Callback是一个callable类,代表回调函数,它有内部状态_context
  4. 这是Callback的构造函数;
  5. call()方法即回调函数的具体实现。

策略模式

callable类可用来实现设计模式里的策略模式

比如商场对不同等级的客户有不同的折扣。

classDiagram
  ShoppingCart o--> DiscountStrategy 

  DiscountStrategy <|-- VipDiscount
  DiscountStrategy <|-- RegularDiscount
  DiscountStrategy <|-- NoDiscount
  
  class ShoppingCart {
    - discountStrategy
    + double calculatePrice(double originalPrice)
  }
  
  class DiscountStrategy {
    <<interface>> 
    + double call(double originalPrice)
  }

// ex465.dart
abstract interface class DiscountStrategy {
  double call(double originalPrice); // 1
}

class VipDiscount implements DiscountStrategy {
  @override
  double call(double originalPrice) => originalPrice * 0.20; // 2
}

class RegularDiscount implements DiscountStrategy {
  @override
  double call(double originalPrice) => originalPrice * 0.10; // 2a
}

class NoDiscount implements DiscountStrategy {
  @override
  double call(double originalPrice) => 0.0; // 2b
}

class ShoppingCart {
  DiscountStrategy _discountStrategy; // 3

  ShoppingCart(this._discountStrategy);

  set discountStrategy(DiscountStrategy strategy) => // 3a
      _discountStrategy = strategy;

  double calculatePrice(double originalPrice) => // 4
      originalPrice - _discountStrategy(originalPrice);
}

void main() {
  const price = 100.0;
  final cart = ShoppingCart(RegularDiscount());
  print('Price with regular discount: \$${cart.calculatePrice(price)}');
  // Output: Price with regular discount: $90.0

  cart.discountStrategy = VipDiscount();
  print('Price with vip discount: \$${cart.calculatePrice(price)}');
  // Output: Price with vip discount: $80.0

  cart.discountStrategy = NoDiscount();
  print('Price with no discount: \$${cart.calculatePrice(price)}');
  // Output: Price with no discount: $100.0
}
  1. DiscountStrategy 是折扣策略的接口(interface);
  2. VipDiscountRegularDiscountNoDiscount是3个具体的折扣策略,它们都实现(implements)了DiscountStrategy ;
  3. ShoppingCart代表购物车,它聚合了一个 DiscountStrategy ;
  4. calculatePrice方法负责计算最终的价格(原价 - 折扣);

枚举

枚举(Enum)是一种用于表示固定数量的常量值的特殊类(class),在第1章数据类型一节中已经简单介绍过,文本将更详细地讨论它。

简单枚举

使用关键字enum声明一个简单枚举如下:

// ex14d.dart
enum Color { red, green, blue }

void main() {
  print(Color.red); // Output: Color.red
  print(Color.blue.name); // Output: blue
  print(Color.blue.index); // Output: 2
}

每个枚举值都有个与之关联的数字,称之为index,该数字从0开始。此例中 red, green, blueindex 分别为0、1、2,可见index是按枚举值的声明顺序依次分配的。

详尽性检查

当枚举值用在switch语句或表达式时,Dart编译器将进行详尽性检查(完备性检查),迫使程序员处理所有的枚举值。

// ex471.dart
enum Color { red, green, blue }

String example(Color color) => switch (color) {
  Color.red => 'Red apple',
  Color.green => 'Green tree',
  Color.blue => 'Blue sky',
};

void main() {
  print(example(Color.blue)); // Output: Blue sky
}

下面这段代码无法通过编译,因为 Color.blue 没有得到处理。

// ex472.dart
enum Color { red, green, blue }

String example(Color color) => switch (color) {
  Color.red => 'Red apple',
  Color.green => 'Green tree',
};

可使用 wildcard (即通配符 _)匹配剩余的枚举值。

// ex473.dart
enum Color { red, green, blue, orange, pink }

String example(Color color) => switch (color) {
  Color.red => 'Red apple',
  Color.green => 'Green tree',
  _ => 'Whatever',
};

void main() {
  print(example(Color.blue)); // Output: Whatever
}

增强型枚举

枚举是一种特殊的类,它可以有自己的字段、方法和常量构造器,这便是增强型枚举的概念(自Dart 2.17开始引入)。

// ex474.dart
enum Status {
  // 1
  continue_(100, message: 'Continue'),
  ok(200, message: 'OK'),
  movedPermanently(301, message: 'Moved Permanently'),
  notFound(404, message: 'Not Found'),
  badGateway(502, message: 'Bad Gateway');

  final int code; // 2
  final String message; // 2a
  const Status(this.code, {required this.message}); // 3

  static Status of(int code) => // 4
      values.firstWhere((status) => status.code == code);

  bool get is2xx => (code ~/ 100) == 2; // 5

  bool get isOk => this == ok; // 5a

  @override
  String toString() => '$message($code)'; // 6
}

void main() {
  print(Status.values); // 7
  // Output: [Continue(100), OK(200), Moved Permanently(301), Not Found(404), Bad Gateway(502)]

  print(Status.ok); // Output: OK(200)
  print(Status.of(404)); // Output: Not Found(404)
  print(Status.ok.isOk); // Output: true
  print(Status.of(502).is2xx); // Output: false

  Status.of(10);
  // Unhandled exception:
  // Bad state: No element
}
  1. Status是一个枚举,首先列出它所有可能的取值 continue_ok等,这些枚举值是常量对象,通过调用常量构造器得到,例如 ok(200, message: 'OK');
  2. codemessageStatus的实例变量,它们都必须声明为final
  3. Status的常量构造器,code是位置参数,message是命名参数;
  4. of是一个静态方法,将参数code转换为枚举值,此方法可以改为factory构造器;
  factory Status.of(int code) => // 4
      values.firstWhere((status) => status.code == code);
  1. is2xxisOk是两个自定义的getter;
  2. 重写toString()方法;
  3. Status.values是由Dart运行时(runtime)自动创建的getter,返回包含Status所有枚举值的一个列表(List<Status>), 其元素的顺序与它们被声明的顺序一致。

增强型枚举与普通类有着类似的语法,但有如下限制:

  • 实例变量必须是 final 的,包括通过 mixin 添加的变量;
  • 所有的生成构造函数必须是常量构造器,即用 const 进行修饰;
  • 不能继承其它类,因为它自动继承了 Enum
  • 不能重写 indexhashCode和 运算符 ==;
  • 不能声明名为 values 的成员,因为它会与自动生成的values getter 冲突;
  • 枚举的所有实例都必须在开头声明,并且至少得声明一个实例。

这些限制听起来都比较合理,也不难理解,不再赘述。

参考资料

元数据

本章节将讨论Dart的元数据注解及其自定义,以及使用 mirors 包获取元数据等信息。

注解

元数据(metadata)用于给代码添加额外的信息,元数据注解(annotation,以下简称注解)以 @ 字符开头,后接一个常量值。我们在之前的代码中已多次见过 @override,它表示实例成员的重写,通常是一个类重写/实现了某个方法。

// ex412.dart
base class Vehicle { // 1
  void moveForward(int meters) {
    // ...
  }
}

base class Car implements Vehicle { // 2
  @override
  void moveForward(int meters) {
    // ...
  }
}

以下4个注解对所有的Dart代码可用:

  • @override
  • @Deprecated
  • @deprecated
  • @pragma

@Deprecated 标记某个功能已过时。

// ex481.dart
class Motorcycle {
  /// Use [start] instead
  @Deprecated('Use start instead')
  void ignite() {
    start();
  }

  /// Start the engine
  void start() {
    // ...
  }
}

@deprecated (注意首字母d小写),不携带附加的信息(message);事实上从源码中可以看出,它的message 被硬编码为"next release"

// dart-sdk/lib/core/annotations.dart
const Deprecated deprecated = Deprecated("next release");

@pragma 用于与Dart程序协同工作的工具。

自定义注解

可以像下面这样自定义注解。

// ex482.dart
class Table { // 1
  final String name;
  final String pk;

  const Table(this.name, {required this.pk}); // 2
}

@Table('tb_staff', pk: 'id') // 3
class Staff {
  int id;
  String name;
  List<String> skills;

  Staff(this.id, {required this.name, this.skills = const []});
}
  1. 这是一个普通的类 Table,其实例变量 name 表示表名,pk表示主键 ;
  2. Table提供了一个常量构造器,对于注解这是必须的;
  3. 使用注解 @TableStaff 类进行标记。

使用 mirrors

mirrors 包是Dart中的反射工具,类似Java中的java.lang.reflect。mirrors中的3个反射函数:

// dart-sdk/lib/mirrors/mirrors.dart
external InstanceMirror reflect(dynamic reflectee);
external ClassMirror reflectClass(Type key);
external TypeMirror reflectType(Type key, [List<Type>? typeArguments]);

reflect 反射一个实例,reflectClass 反射一个类,reflectType 反射一个类型。到底什么是反射(reflection)?下面通过一个示例来说明。

// ex483.dart
import 'dart:mirrors';

class _Foo {
  const _Foo();
}

class _Bar {
  const _Bar();
}

const foo = _Foo();
const bar = _Bar();

@foo
@bar
class Exam {
  @foo
  String? str;

  @bar
  void exam() {}
}

void main() {
  var examMirror = reflectClass(Exam); // 1
  for (var meta in examMirror.metadata) { // 2
    print(meta.reflectee);
  }
  // Output:
  // Instance of 'Foo'
  // Instance of 'Bar'

  for (var mem in examMirror.instanceMembers.entries) {
    print(mem); // 3
    print('  metadata: ${mem.value.metadata}'); // 4
  }

  // Output:
  // MapEntry(Symbol("=="): MethodMirror on '==')
  //   metadata: [InstanceMirror on Instance of '_Patch', InstanceMirror on Instance of 'pragma', InstanceMirror on Instance of 'pragma', InstanceMirror on Instance of 'pragma', InstanceMirror on Instance of 'pragma']
  // MapEntry(Symbol("hashCode"): MethodMirror on 'hashCode')
  //   metadata: [InstanceMirror on Instance of '_Patch']
  // MapEntry(Symbol("toString"): MethodMirror on 'toString')
  //   metadata: [InstanceMirror on Instance of '_Patch', InstanceMirror on Instance of 'pragma']
  // MapEntry(Symbol("noSuchMethod"): MethodMirror on 'noSuchMethod')
  //   metadata: [InstanceMirror on Instance of 'pragma', InstanceMirror on Instance of 'pragma', InstanceMirror on Instance of '_Patch', InstanceMirror on Instance of 'pragma']
  // MapEntry(Symbol("runtimeType"): MethodMirror on 'runtimeType')
  //   metadata: [InstanceMirror on Instance of '_Patch', InstanceMirror on Instance of 'pragma', InstanceMirror on Instance of 'pragma']
  // MapEntry(Symbol("str"): Instance of '_SyntheticAccessor')
  //   metadata: []
  // MapEntry(Symbol("str="): Instance of '_SyntheticAccessor')
  //   metadata: []
  // MapEntry(Symbol("exam"): MethodMirror on 'exam')
  //   metadata: [InstanceMirror on Instance of '_Bar']
}
  1. 反射Exam类;
  2. Exam类的元数据;
  3. 获取Exam类的实例成员;
  4. 获取Exam类的实例成员的元数据。

参考资料

继承

本章节重点介绍类继承的基本概念,包括子类的声明单继承方法重写noSuchMethod()等。

声明一个子类

子类继承父类,意味着子类继承了父类的实例成员,包括实例字段和实例方法。使用extend关键字创建一个子类。

// ex511.dart
class Mammal {
  String name; // 1

  Mammal(this.name); // 2

  void play() => print('$name is playing'); // 3

  void sleep() => print('$name is sleeping'); // 3a
}

// 4
class Dolphin extends Mammal {
  Dolphin(super.name); // 5
}

void main() {
  var dolphin = Dolphin('Dolphin'); // 6
  print(dolphin.name); // 7 Output: Dolphin
  dolphin.play(); // 7a Output: Dolphin is playing
  dolphin.sleep(); // 7b Output: Dolphin is sleeping
}
  1. Mammal 类有一个实例变量 name
  2. 这是 Mammal 类的构造函数;
  3. Mammal 类有2个实例方法,playsleep
  4. Dolphin 继承了 Mammal 类;
  5. Dolphin 类的构造函数,使用 super 关键字引用父类;
  6. 实例化 Dolphin 类;
  7. Dolphin 类从 Mammal 类继承了实例成员,包括1个实例变量和2个实例方法。

除了 Null 之外,Dart中的每个类都隐式地继承了 Object 类。此例中的 Mammal 类即:

class Mammal extends Object {
  // ... (omitted for brevity)
}

但这里的 extends Object 完全没必要写出来。

单继承

Dart只允许单继承,不支持多继承,即一个类只能继承一个父类。

// ex512.dart
import 'ex511.dart';

// Error: Each class definition can have at most one extends clause.
// Try choosing one superclass and define your class to implement (or mix in) the others.
// class CuteDolphin extends Dolphin, Mammal  {
//                                  ^
class CuteDolphin extends Dolphin, Mammal  {
  CuteDolphin(super.name);
}

这里 CuteDolphin 试图同时继承 DolphinMammal 类,导致编译失败。 解决方法即让 CuteDolphin 只继承 Dolphin 类。

class CuteDolphin extends Dolphin {
  CuteDolphin(super.name);
}

再谈构造函数

每个类的生成式构造函数被执行时,一定会或显式或隐式地先执行父类的生成式构造函数,即构造函数的第一行代码是调用父构造函数。

// ex513.dart
import 'ex511.dart';

// Error: The implicitly called unnamed constructor from 'Mammal' has required parameters.
// Try adding an explicit super initializer with the required arguments.
//   Ape();
//   ^
class Ape extends Mammal {
  Ape();
}

// Error: The superclass, 'Mammal', has no unnamed constructor that takes no arguments.
// class Elephant extends Mammal {}
//       ^
class Elephant extends Mammal {}

Ape 类的构造函数 Ape(),隐式地调用父类的无参构造函数 Mammal() 引起编译错误,编译器提示Mammal类的unnamed构造函数需要required参数。

Elephant 类默认的构造函数Elephant(),隐式地调用父类的无参构造函数 Mammal() ,编译器提示这样的父构造函数不存在。

方法重写

子类可以重写(override)实例方法(包括运算符)、getter 和 setter。使用 @override 注解来表明重写意图。

// ex514.dart
import 'ex511.dart';

class Badger extends Mammal {
  Badger(super.name);

  @override
  void play() => print('$name Badger is playing');
}

void main() {
  var bader = Badger('B');
  bader.play(); // Output: B Badger is playing
}

重写方法(A)的声明必须与被重写方法(B)在以下几个方面相匹配:

  • A的返回类型必须与B的相同(或为其子类型);
  • A的参数类型必须与B的相同(或为其超类型);
  • 如果B接受 n 个位置参数,则A也必须接受 n 个位置参数;
  • 泛型方法不能重写非泛型方法,反之亦然(泛型乃第6章内容)。
// ex515.dart
import 'ex511.dart';

class Dog extends Mammal {
  Dog(super.name);

  Object sing(int times) => "$name: ${'woof~ ' * times}";
}

class Corgi extends Dog {
  Corgi(super.name);

  @override
  String sing(num times) => "$name: ${'woooof~ ' * times.toInt()}";
}

void main() {
  var corgi = Corgi('Rover');
  print(corgi.sing(3)); // Output: Rover: woooof~ woooof~ woooof~
}

请仔细观察此例中 Corgi.sing(int) 实例方法如何重写了 Dog.sing(int)

NOTE: 如果重写 ==,也应当重写 Object.hashCode getter, 原因与Map的实现有关。

noSuchMethod()

先看一个例子。

// ex516.dart
class Fox {}

void main() {
  dynamic f = Fox();
  f.sing();
  // Unhandled exception:
  // NoSuchMethodError: Class 'Fox' has no instance method 'sing'.
  // Receiver: Instance of 'Fox'
  // Tried calling: sing()
}

此程序调用一个不存在的方法 'f.sing()', 引起运行时异常: NoSuchMethodError: Class 'Fox' has no instance method 'sing',这是Object 类中的 noSuchMethod 方法的默认行为,可以重写它进而实现一些特殊的功能。

// ex517.dart
class Gorilla {
  @override
  dynamic noSuchMethod(Invocation invocation) {
    return """you're invoking ${invocation.memberName}
  method             : ${invocation.isMethod}  
  positionalArguments: ${invocation.positionalArguments}
  namedArguments     : ${invocation.namedArguments}""";
  }
}

void main() {
  dynamic g = Gorilla();
  print(g.sing('a song'));
  // Output:
  // you're invoking Symbol("sing")
  // method             : true  
  // positionalArguments: [a song]
  // namedArguments     : {}
}

参考资料

接口

接口(interface)即代码之间的契约。 本章节将结合示例讨论Dart中的隐式接口接口类纯接口等概念。

隐式接口

广义上一个library暴露一些成员(函数、类、字段、方法等)给另一个library使用,这些暴露的成员便组成了接口。Dart的普通类隐式地定义了一个接口。

// ex521.dart
class Vehicle {
  void moveForward(double meters) {
    print('moving forward $meters meters');
  }
}

class Car implements Vehicle {
  @override
  void moveForward(double meters) {
    // TODO: implement moveForward
  }
}

此例中 Car 实现了 Vehicle , 因此它必须重写 moveForward(double) 方法,否则无法通过编译。Vehicle 隐式地定义了如下接口:

abstract interface class IVehicle {
  void moveForward(double meters);
}

abstract interface class 表示纯接口,概念上与Java中的interface对应。

接口类

使用 interface class 创建一个接口类(或简称接口)。

// ex522.dart
interface class Vehicle {
  void moveForward(double meters) {}
}

外部library中的类可以实现接口类,但不能继承接口类。

// ex523.dart
import './ex522.dart';

class Car implements Vehicle {
  @override
  void moveForward(double meters) {
    // TODO: implement moveForward
  }
}

// Error: The class 'Vehicle' can't be extended outside of its library
// because it's an interface class.
class Truck extends Vehicle {}

实现接口类,意味着要重写接口类定义的所有公开的实例方法。不能继承接口类,意味着无法通过 super关键字调用接口类定义的实例方法。接口类保证了:

  • 实现类的某个实例方法A,通过this关键字调用另一个实例方法B时,A与B始终位于同一个library中;
  • 实现类无法通过super调用接口类中的实例方法,接口类里的实例方法的具体实现不直接影响实现类,这减少了脆弱基类问题(fragile base class problem)

纯接口

纯接口即抽象接口类(abstract interface class),它的实例方法通常是抽象方法,即只有方法签名没有具体实现。

// ex524.dart
abstract interface class Vehicle {
  void moveForward(double meters);
}

纯接口中的实例方法只能是抽象方法吗?答案是否定的。

abstract interface class Vehicle2 {
  void moveForward(double meters);

  void moveBackward(double meters) {
     print('move backward $meters meters');
  }
}

Mixin

Mixin 是一种代码复用技术,旨在为类添加新成员。简单地说,mixin 是可复用的代码片段。 本章节将介绍mixin基础mixin类,以及解决mixin成员依赖问题的3种策略。

Mixin 基础

使用 mixin 关键字声明 mixin。

// ex531.dart
mixin Starter {
  var started = false;

  void start() => started = true;
}

在声明类时,使用关键字 with 为其添加 mixin。

// ex532.dart
import './ex531.dart';

class Game with Starter {}

class Engine with Starter {}

void main() {
  var g = Game();
  print(g.started); // Output: false
  g.start();
  print(g.started); // Output: true
}

with 子句可与 extends 子句连用,且可以为一个类添加多个mixin。

// ex533.dart
import './ex531.dart';

mixin Leveler {
  int level = 0;
}

class Fun {}

class Game extends Fun with Starter, Leveler {}

void main() {
  var g = Game();
  g.level = 3;
  g.start();
  print('${g.level} ${g.started}'); // Output: 3 true
}

mixin 不能有 extendswith 子句,也不能声明构造函数。

Mixin 类

使用 mixin class 声明一个 mixin 类。mixin类可同时作为类和mixin使用,但也有更多的限制,实际中它的应用不多。

// ex534.dart
mixin class Player {
  late String playerName;
}

mixin 类不能有 extendswithon 子句,也不能定义生成式构造函数。

Mixin 成员依赖

有时候一个mixin需要访问一些不由它自己定义的成员(字段/方法)。换句话说,使用该mixin的类需要或显示或隐式地实现某个接口,这个接口正好包含了mixin所需的成员。下面讨论解决成员依赖的3种策略。

策略1:在mixin中定义抽象成员

// ex535.dart
mixin Adder {
  set count(int value); // 1
  int get count; // 1a

  void add(int amount) => count += amount; // 2
}

// 3
class AddCounter with Adder {
  @override
  int count = 0;
}

void main() {
  var c = AddCounter();
  c.add(2);
  assert(c.count == 2);
}
  1. Adder mixin 为属性count定义了抽象的getter和setter;
  2. Adder.add(int) 方法调用了count的getter和setter;
  3. AddCounter 类使用了 Adder mixin,且实现了count的getter和setter两个抽象方法。

策略2:让mixin实现一个接口

让 mixin 实现一个接口,使用该 mixin 的类也自然地实现了该接口。对上述 Adder mixin 进行重构,对它的两个抽象方法(即count的getter和setter),抽取至一个接口中,形成如下版本。

// ex536.dart
abstract interface class Countable {
  set count(int value); // 1
  int get count; // 1a
}

mixin Adder implements Countable {
  void add(int amount) => count += amount; // 2
}

// 3
class AddCounter with Adder {
  @override
  int count = 0;
}
  1. Countable 接口定义了属性count的getter和setter;
  2. Adder mixin 实现了 Countable
  3. AddCounter 类使用了 Adder mixin,自然地实现了Countable接口。

可以向下面这样声明AddCounter类:

class AddCounter with Adder implements Countable {
 // ... (omitted for brevity)
}

策略3:使用 on 子句定义超类

在声明mixin是使用on子句,用以限制mixin应用于哪些类。例如:

mixin Adder on Counter {
}

只有Counter的子孙类才可以使用 Adder mixin。同时,Adder 也可以访问 Counter 中的成员(字段/方法)。让Counter 隐式地实现 Countable 接口,形成如下新版本。

// ex537.dart
class Counter {
  int count = 0; // 1
}

mixin Adder on Counter {
  void add(int amount) => count += amount; // 2
}

class AddCounter extends Counter with Adder {} // 3

参考资料

类修饰符

上一章节讨论了MixinMixin 类。 本章节将重点讨论类的修饰符及其组合

abstract

abstract class 用于声明抽象类,抽象类可包含抽象方法(即只有签名没有具体实现的方法),且不能被实例化。

// ex447.dart
abstract class Mammal {
  void breathe();
}

interface

使用 interface class 创建一个接口类(或简称接口),详见接口一节。

base

每个类(class)都隐式地定义了一个接口(interface),因而可以实现(implements)一个类(即实现类中的方法)。base 类为继承而生,在定义它的library之外,只能被继承,而不能被实现。

ex541.dart:

// ex541.dart
base class Vehicle { // 1
  void moveForward(int meters) {
    // ...
  }
}

base class Car implements Vehicle { // 2
  @override
  void moveForward(int meters) {
    // ...
  }
}

base class Motorcycle extends Vehicle { // 3
 
}

ex541a.dart:

// ex541a.dart
import 'ex541.dart';

base class Truck extends Vehicle { // 4
  int passengers = 4;

  @override
  void moveForward(int meters) {
    // ...
  }
}

// ERROR: 
// The class 'Vehicle' can't be implemented outside of its library 
// because it's a base class.
base class Train implements Vehicle { // 5
  @override
  void moveForward(int meters) {
    // ...
  }
}
  1. ex541.dart 文件中的Vehicle是一个 base 类,它有一个实例方法 moveForward(int);
  2. 同一文件中的 Car 实现了 Vehicle;
  3. 同一文件中的 Motorcycle 继承(extends)了 Vehicle;
  4. 另一文件( ex541a.dart )中的 Truck 继承了 Vehicle;
  5. 另一文件( ex541a.dart )中的 Train 试图实现 Vehicle,引发编译错误。

base 类有如下特点:

  • base类的子类/实现类必须使用basefinalsealed修饰,这是为了确保base的语义;
  • base类的子类自动继承了其可见的成员(字段/方法),因此为base类添加新的成员不会破坏子类,除非新成员与子类发生冲突;
  • 实例化base类的子类,必然导致base类的构造函数被调用。

sealed

sealed class用于创建密封类。密封类的子类是可穷举的,且必须与密封类位于同一个libary里。密封类是隐式抽象的,不能被实例化。

// ex344.dart
import 'dart:math' as math;

sealed class Shape {}

class Square implements Shape {
  final double length;
  Square(this.length);
}

class Circle implements Shape {
  final double radius;
  Circle(this.radius);
}

double calculateArea(Shape shape) => switch (shape) {
  Square(length: var l) => l * l,
  Circle(radius: var r) => math.pi * r * r,
};

密封类的一个特点是switch详尽性检查。假如还有一个类也实现或继承了 Shape,如:

class Rectangle extends Shape {}

calculateArea(Shape)函数会有编译错误:

 Error: The type 'Shape' is not exhaustively matched by the switch cases since it doesn't match 'Rectangle()'.
 - 'Shape' is from 'bin/ch03/ex344.dart'.
Try adding a wildcard pattern or cases that match 'Rectangle()'.
double calculateArea(Shape shape) => switch (shape) {
                                             ^

final

final类不能被外部libary继承或实现。final类的子类或实现类也必须是final类。

ex542.dart

// ex542.dart
final class Pet {}

final class Kitty extends Pet {}

final class Garfield implements Pet {}

ex542a.dart

// ex542a.dart
import 'ex542.dart';

// Error: The class 'Pet' can't be extended outside of its library
// because it's a final class
final class Doggy extends Pet {}

// Error: The class 'Pet' can't be implemented outside of its library
// because it's a final class.
final class Corgi implements Pet {}

由于final类只能被同一个library中的类继承或实现:

  • 你可以安全地对其进行修改;
  • 也不用担心其实例方法被其它的library重写。

修饰符的组合

下表列出了有效的修饰符的组合及其特性。

声明可实例化?可继承?可实现?Mixin?可穷举?
class✔️✔️✔️
base class✔️✔️
interface class✔️✔️
final class✔️
sealed class✔️
abstract class✔️✔️
abstract base class✔️
abstract interface class✔️
abstract final class
mixin class✔️✔️✔️✔️
base mixin class✔️✔️✔️
abstract mixin class✔️✔️✔️
abstract base mixin class✔️✔️
mixin✔️✔️
base mixin✔️

参考资料

扩展方法

想给一个三方的libray添加一些新功能,比如像下面这样,将一个字符串转换为整数:

  "1".toInt()

String 类中并没有 toInt() 这个方法,而且String类位于Dart核心库中,我们不可能对它进行修改进而添加新方法。这便是扩展方法(extension methods,简称扩展)的用武之地。

声明扩展

使用 extension ... on ... 语法声明一个扩展。

// ex551.dart
extension NumParse on String {
  int toInt({int? radix}) => int.parse(this, radix: radix);

  double toDouble() => double.parse(this);
}

void main() {
  assert(10 == "10".toInt());
  assert(1.1 == "1.1".toDouble());
}

这里的NumParse为扩展名,如果NumParse只是在定义它的library中使用,该名称可以省略。

extension on String {
  // ... (omitted for brevity)
}

使用扩展方法

NumParseString添加了两个扩展方法 toInt()toDouble(),就像String自身定义的其它方法一样,直接调用它们即可。

// ex552.dart
import 'ex551.dart';

void main() {
  assert(10 == "a".toInt(radix: 16));
}

假设另一个String的扩展 NumParse2,它定义的方法与NumParse存在冲突:

// ex551a.dart
extension NumParse2 on String {
  int toInt() => int.parse(this);

  double toDouble() => double.parse(this);

  num toNum() => contains('.') ? toDouble() : toInt();
}

但你的程序又要同时倒入它们所在的library,怎么办?下面介绍避免此类冲突的两种方法。

方法1:使用show/hide限制暴露的API

// ex553.dart
import 'ex551.dart';
import 'ex551a.dart' hide NumParse2;

void main() {
  print('abc'.toInt(radix: 16)); // Output: 2748
}

方法2: 将扩展看做包装类

// ex554.dart
import 'ex551.dart';
import 'ex551a.dart';

void main() {
  print(NumParse("123").toInt()); // 1 Output: 123
  print(NumParse2("456").toDouble()); // 2 Output: 456.0
  print("789".toNum()); // 3 Output: 789
}
  1. 显示地调用 NumParsetoInt()方法,NumParse("123")看起来就像调用了一个构造函数,而NumParse就像一个包装类;
  2. 这行代码与上一行类似;
  3. toNum()方法只在NumParse2扩展中被定义了,因此直接调用也不会产生歧义。

如果两个扩展的名称相同,就可能要使用 前缀导入,即给导入的libray取一别名。

// ex555.dart
import 'ex551.dart' as lib1;
import 'ex551b.dart' as lib2;

void main() {
  print(lib1.NumParse('def').toInt(radix: 16)); // Output: 3567
  print(lib2.NumParse('234').toDouble()); // Output: 234.0
  print("345".toNum()); // Output: 345
}

参考资料

扩展类型

扩展类型(Extension types)是一种编译时抽象(仅存在编译时期),是对现有类型的一种静态包装。它们是静态 JS 互操作(static JS interop)的重要组件,因为它们可以轻松修改现有类型的接口,而无需产生包装类的成本。

扩展类型对底层类型(称为表示类型,representation type)对象可用的操作集(或接口)强制进行约束。定义扩展类型时,可以复用表示类型的某些成员、省略/替换其它成员、以及添加新功能,这有点类似于设计模式里的外观模式

声明

使用 extension type 关键字声明扩展类型。

// ex561.dart
extension type IdNumber(int i) {} // 1

void main() {
  var id = IdNumber(123); // 2
  print(id); // 3 Output: 123
  print(id.i); // 3a Output: 123
  print(id.runtimeType); // 4 Output: int
}
  1. 声明扩展类型 IdNumber, 其表示类型为 int,同时也隐式地定义了
  • 构造函数 IdNumber(this.i)
  • 实例变量 final int i
  1. 声明变量 id,赋值为IdNumber(123)
  2. 分别打印 idid.i
  3. 打印 id的运行时类型,结果为 int,这印证了IdNumber在编译过程中被抹掉了。

构造函数

命名构造函数

除了隐式的构造函数外,还可以为扩展类型定义命名构造函数。

// ex562.dart
extension type IdNumber(int i) {
  IdNumber.of(String str) : i = int.parse(str);
}

void main() {
  var x = IdNumber(123);
  var y = IdNumber.of('123');
  assert(x == y);
}

采用命名构造函数的形式声明

使用命名构造函数的形式声明扩展类型时,可以灵活的定义不具名构函数(可选的)。

// ex563.dart
extension type IdNumber.of(int i) {
  IdNumber(String str) : i = int.parse(str);
}

void main() {
  var x = IdNumber.of(123);
  var y = IdNumber('123');
  assert(x == y);
}

对外隐藏不具名构造函数

// ex564.dart
extension type const IdNumber._(int i) {
  const IdNumber.of(int i) : this._(i);
}

此例也演示了扩展类型如何声明 const 构造函数。

工厂构造函数

// ex565.dart
extension type IdNumber._(int i) {
  factory IdNumber.of(int i) => IdNumber._(i);
}

成员

扩展类型不能定义 non-external 实例字段和抽象成员,除此之外可定义下列成员:

  • 方法
  • getter
  • setter
  • 操作符(operators)
// ex566.dart
extension type N(int i) {
  N operator +(N m) => N(i + m.i);
  int get n => i;
  bool isPositive() => i > 0;
}

void main() {
  var n = N(100);
  print(n + N(23)); // Output: 123
  print(n.n); // Output: 100
  print(n.isPositive()); // Output: true
}

实现接口

扩展类型只可实现:

实现表示类型

一个扩展类型实现它的表示类型时,它便自动拥有了表示类型的所有的成员。

// ex567.dart
import 'package:meta/meta.dart';

extension type N(int i) implements int {
  bool get isPositive => i > 0;

  @redeclare
  N operator +(int n) => N(i + n);
}

void main() {
  var a = N(10);
  var b = N(20);
  var c = a + b;

  print('${c.runtimeType} $c'); // Output: int 30
  print('${a - b} ${a * b} ${a / b}'); // Output: -10 200 0.5
  print('${a.isPositive} ${b.isNegative}'); // Output: true false

  print(a + 1); // Output: 11
  print(2 + a); // Output: 12
}

此例中扩展类型 N 是 类型 int 的透明包装,同时:

  • 添加了新的 getter(isPositive);
  • 重新定义了 + 操作符。

注:meta package 中的注解@redeclare用于告诉编译器你有意地使用与超类相同的成员名称,如果事实并非如此,将收到一个警告。

实现表示类型的超类

// ex568.dart
extension type N(int i) implements num {} // 1

void main() {
  var a = N(10);
  var b = N(20);
  var c = a + b; // 2
  print('${c.runtimeType} $c'); // Output: int 30
  print(1 & 2); // 3 Output: 0

  print(a & b); // 3a
  // Error:
  // The operator '&' isn't defined for the type 'N'.
  // Try defining the operator '&'.
}
  1. 扩展类型 N 实现了类型 num
  2. num 定义了 + 操作符,因此 N 也自然地拥有了 + 操作符;
  3. int 定义了 & 操作符,因此 1 & 2 这样的表达式是有效的;
    numN 都没有定义 & 操作符,因而 a & b 将引起编译错误。

实现基于同一表示类型的其它扩展类型

// ex569.dart
extension type A(num n) {
  num get square => n * n; // 1
}

extension type B(num n) {
  num get cube => n * n * n; // 2
}

extension type C(num n) implements A, B {} // 3

void main() {
  var n = C(3);
  print(n.runtimeType); // Output: int
  print(n.square); // Output: 9
  print(n.cube); // Output: 27
}
  1. num的扩展类型 A定义了一个 名为 square 的 getter;
  2. num的扩展类型 B定义了一个 名为 cube 的 getter;
  3. num的扩展类型 C同时实现了AB,拥有了来自AsquareBcube,这类似于多重继承。

参考资料

泛型

泛型即参数化类型,它是一个既常见又重要的编程语言特性。本文将举例说明泛型的优势和具体使用方法。

类型安全

泛型的优势之一是类型安全

// ex611.dart
void main() {
  var list = [];
  list.add(1);
  list.add('A');
  print('$list ${list.runtimeType}'); // Output: [1, A] List<dynamic>
}

此例中list的类型为List<dynamic>,表示list可以包含任何类型的数据。如果将list声明为 List<int>将引起编译错误。

// ex611a.dart
void main() {
  var list = <int>[];
  list.add(1);
  list.add('A');
  // Error: The argument type 'String' can't be assigned to the parameter type 'int'.
}

减少代码重复

泛型的另一个优势是减少代码重复。实战中经常要将来自设备或网络的数据缓存起来。下面的代码创建一个用于缓存String数据的类。

// ex612.dart
class StringCache {
  final String data;

  StringCache(this.data);
}

使用相同的代码结构可为doubleint及其它数据类型的数据创建对应的缓存类。例如:

// ex612a.dart
class DoubleCache {
  final double data;

  DoubleCache(this.data);
}

通过使用泛型技术我们只需要创建一个类,就可以 cover 此类应用场景。

// ex613.dart
class Cache<T> { // 1
  final T data; // 2

  Cache(this.data);
}

void main() {
  var str = Cache('Dart'); // 3
  var num = Cache(12); // 4
  assert(str.data == 'Dart');
  assert(num.data == 12);
}
  1. 泛型的类型参数T写在一对尖括号(<>)中;
  2. 字段data的数据类型被声明为T
  3. 得益于 Dart 的自动类型推断,Cache('Dart')Cache<String>('Dart'),类型参数T将在编译时被替换为String
  4. 同上,Cache(12)Cache<int>(12)

限制参数化类型

使用extends关键字让参数化类型为指定类型的子类型。

// ex614.dart
class Cache<T extends Object> {
  final T data;

  Cache(this.data);
}

void main() {
  Cache(null);
  // Error: The argument type 'Null' can't be assigned to the parameter type 'Object'.
}

此例中的TObject 的子类,它不能是 Null 类型。

泛型方法

参数化类型也可用在方法或函数里,语法是将类型参数连通一对尖括号,写在方法或函数名后面。

// ex615.dart
class Example {
  static T max<T extends Comparable<T>>(T a, T b) => a.compareTo(b) > 0 ? a : b;
}

T min<T extends Comparable<T>>(T a, T b) => a.compareTo(b) < 0 ? a : b;

void main() {
  print(Example.max(12, 34)); // Output: 34
  print(min('apple', 'orange')); // Output: apple
}

参考资料

集合概述

Dart 中的集合数据类型(Collection)包括 List、Set、Map 和 Queue。

classDiagram
  Iterable~E~ <|.. List~E~
  Iterable~E~ <|.. Set~E~
  Iterable~E~ <|.. Queue~E~

  class Iterable{
    <<abstract mixin>>
    + Iterator~E~ get iterator
  }
  <<interface>> List
  <<interface>> Set
  <<interface>> Queue

  Iterable~E~ ..> Iterator~E~ : Creates
  Iterable <-- Map~K,V~

  class Iterator {
    <<interface>>
    + E get current
    + bool moveNext()
  }

  class Map{
    <<interface>>
    + Iterable~MapEntry&lt;K, V&gt;~ get entries
    + Iterable~K~ get keys
    + Iterable~V~ get values
  }

Iterable

Iterable(可迭代对象)是一个抽象类,它是所有集合类(如 List 和 Set)的基石。点击这里查看 Iterable生成器

Iterable 的大多数方法(如 mapwhere)都是惰性的。这意味着当你调用这些方法时,它们并不会立即遍历集合。只有当你最终调用 toList()toSet() 或在 for-in 循环中使用它时,计算才会真正发生。这种机制在处理大数据集时非常高效,因为它能避免不必要的中间计算。

核心属性

  • first: 返回第一个元素。
  • last: 返回最后一个元素。
  • single: 检查是否只有一个元素并返回它。
  • isEmpty: 集合是否为空。
  • isNotEmpty: 集合是否不为空。
  • length: 返回元素个数。
  • iterator: 获取用于遍历的 Iterator 对象。

过滤

  • where(test): 返回满足条件的元素集合。
  • whereType<T>(): 筛选出指定类型的元素(如 list.whereType<String>())。
  • take(n): 获取前 n 个元素。
  • takeWhile(test): 从头开始取,直到条件不满足。
  • skip(n): 跳过前 n 个元素。
  • skipWhile(test): 从头开始跳过,直到条件不满足。

转换

  • map(toElement): 将每个元素转换为另一种形式。
  • expand(toElements): 将每个元素转换为一个序列,然后展平。
  • cast<R>(): 将 Iterable 强制转换为另一种类型的 Iterable。
  • followedBy(other): 将另一个集合接在当前集合后面。
  • toList({growable: true}): 转换为 List(触发惰性求值)。
  • toSet(): 转换为 Set(去重并触发惰性求值)。
  • toString(): 返回集合的字符串表示。

查找

  • firstWhere(test, {orElse}): 查找第一个满足条件的元素。
  • lastWhere(test, {orElse}): 查找最后一个满足条件的元素。
  • singleWhere(test, {orElse}): 查找唯一满足条件的元素。
  • elementAt(index): 获取指定索引处的元素。
  • contains(element): 判断是否包含某个元素。

逻辑检查

  • any(test): 是否至少有一个元素满足条件。
  • every(test): 是否所有元素都满足条件。

聚合

  • fold(initialValue, combine): 提供初始值,并从左到右迭代合并。
  • reduce(combine): 无初始值,将集合元素两两合并(要求集合非空)。
  • join(separator): 将所有元素转为字符串并用分隔符连接。

List

List 是一组有序的对象(元素)的集合,可以通过下标(从 0 开始的整数)获取对应位置的元素。

// ex621.dart
void main() {
  var letters = ['A', 'B', 'C', 'A'];
  assert(4 == letters.length);
  assert('A' == letters[0]);
  assert('B' == letters[1]);
  assert('C' == letters[2]);
  print(letters); // Output: [A, B, C, A]
  print(letters.toSet()); // Output: {A, B, C}
}

Set

Set 不关心元素的排序顺序,它关注的是元素是否在集合里,Set 中的元素不会重复。

// ex622.dart
void main() {
  var letters = {'A', 'B', 'C', 'A'};
  assert(3 == letters.length); // 1
  print(letters); // 2 Output: {A, B, C}
  print(letters.toList()); // 2a Output: [A, B, C]
}
  1. letters的长度为 3 而不是 4;
  2. letters包含的 3 个元素即 {A, B, C}A 只出现了一次。

Queue

Queue 表示队列,它提供了队列两端的添加/移除操作。

// ex623.dart
import 'dart:collection';

void main() {
  var q = Queue.of(['A', 'B', 'C']);
  print(q); // Output: {A, B, C}

  q.addFirst('X');
  print(q); // Output: {X, A, B, C}

  q.addLast('Y');
  print(q); // Output: {X, A, B, C, Y}

  q.removeLast();
  print(q); // Output: {X, A, B, C}

  q.removeFirst();
  print(q); // Output: {A, B, C}
}

Map

Map 是键值(key-value)对的集合,它表达了 key 到 value 的映射关系。

// ex624.dart
void main() {
  var prices = {'Apple': 1.2, 'Banana': 1.4, 'Cherry': 2.1};
  print(prices.runtimeType); // Output: _Map<String, double>
  print(prices['Apple']); // Output: 1.2

  prices['Apple'] = 1.5;
  prices['Damson'] = .5;
  print(prices); // Output: {Apple: 1.5, Banana: 1.4, Cherry: 2.1, Damson: 0.5}

  print(prices.keys); // Output: (Apple, Banana, Cherry, Damson)
  print(prices.values); // Output: (1.5, 1.4, 2.1, 0.5)
  print(prices.entries);
  // Output: (MapEntry(Apple: 1.5), MapEntry(Banana: 1.4), MapEntry(Cherry: 2.1), MapEntry(Damson: 0.5))
}

集合元素

以 List 为例,简单说明下集合元素。在以[a, b, ...]这样的语法书写 List 时,其中的ab等称为元素(elements);每个元素将被求值(evaluated)以产生 0 到多个值(value),这些值随后将被插入到结果 List 中。集合元素分为叶元素(leaf elements)和控制流元素(control flow elements)两类。控制流元素即collection if/for,是必须掌握的重要编程技巧,它在Flutter开发中使用的极为广泛。

// ex633.dart
void main() {
  print([1 * 1, 2 * 2, 3 * 3]); // 1 Output: [1, 4, 9]

  int? a;
  print([1, 2, a, 3]); // 2 Output: [1, 2, null, 3]
  print([1, 2, ?a, 3]); // 2a Output: [1, 2, 3]

  final arr = [3, 4];
  var arr2 = [1, 2, ...arr, 5]; // 3
  print('${arr2.runtimeType} $arr2'); // Output: List<int> [1, 2, 3, 4, 5]

  List<int>? arr3;
  print([1, 2, ...?arr3, 3]); // 4 Output: [1, 2, 3]
}
  1. 此行代码展示的是表达式元素(Expression elements),它属于叶元素;
  2. 这行中的 ?a 是 Null-aware 元素,请与上一行代码进行对比;
  3. 这行中的 ...arr 乃 Spread 元素;
  4. 这行中的 ...?arr3 乃 Null-aware Spread 元素。

if 元素

if元素 和 for元素 都是控制流元素。下面的示例展示了if元素的各种用法。

// ex634.dart
void main() {
  var present = true;
  print([0, if (present) 1, 2, 3]); // 1 Output: [0, 1, 2, 3]
  print([0, if (!present) 1, 2, 3]); // 1a Output: [0, 2, 3]

  var letter = 'A';
  print([
    if (letter == 'A') 'apple' else 'orange',
    'orange',
  ]); // 2 Output: [apple, orange]

  print([
    if (letter == 'A') 'apricot' else if (letter == 'O') 'orange',
    'orange',
  ]); // 3 Output: [apricot, orange]

  var data = ['apple', 0.2];
  print([
    if (data case [var name, var price])
      'The price of $name is \$$price'
    else
       'Error data',
  ]); // 4 Output: [The price of apple is $0.2]
}

for 元素

如下示例简单展示了for元素的用法。

// ex635.dart
void main() {
  var numbers = [for (var i = 2; i <= 8; i += 2) i, 10]; // 1
  print(numbers); // Output: [2, 4, 6, 8, 10]

  var squares = [for (var n in numbers) n * n]; // 2
  print(squares); // Output: [4, 16, 36, 64, 100]
}

嵌套控制流元素

控制流元素可以互相嵌套。

// ex636.dart
void main() {
  var evens = [
    for (var i = 0; i < 10; i++)
      if (i % 2 == 0) i,
  ];
  print(evens); // Output: [0, 2, 4, 6, 8]

  var numbers = [
    for (var i = 0; i < 10; i++)
      if (i % 3 == 0)
        for (var j = i; j < i + 3; j++)
          if (j < 10) j,
  ];
  print(numbers); // Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}

List(列表)

数组(array)是一组有序的对象(元素)的集合,意味着可以通过下标(从 0 开始的整数)访问数组内对应位置的元素。Dart 的数组即 List,在之前的章节已经多次出现过。

List 有各种不同类型的实现子类,最常见的两类是:

  • 固定长度的(Fixed-length) List:当执行可能改变 List 长度的操作时将引发错误
  • 可增长的(growable) List

List 具有丰富的 API:

  • 操作符 []=, addaddAll 向 List 添加数据。
  • indexOflastIndexOf 检查元素是否在 List 中及其对应位置。
  • remove, removeAt, removeLast, removeRangeremoveWhere 从 List 中移除元素。
  • insertinsertAll向将元素插入到 List 的指定位置。
  • fillRange, replaceRangesetRange 替换 List 某区间内的元素。
  • sort 对 List 中的元素进行排序。
  • shuffle 将 List 中的元素随机打乱。
  • firstWhere 查找满足条件的首个元素,可以指定一个查找失败时的默认值,类似地还有 lastWheresingleWhere 方法。

List 基础

先来看一个简单的示例。

// ex631.dart
void main() {
  var numbers = [1, 2, 3]; // 1 List<int>
  print(numbers); // 2 Output: [1, 2, 3]
  assert(1 == numbers[0]); // 3
  numbers[1] = 9; // 4
  print(numbers); // 5 Output: [1, 9, 3]
  print(numbers.length); // 6 Output: 3
  print(numbers[3]); // 7
  // Unhandled exception:
  // RangeError (length): Invalid value: Not in inclusive range 0..2: 3
}
  1. 使用字面量创建一个 List,其元素放在一对中括号([])内,并以逗号(,)分割;numbers是一个List<int>
  2. 打印numbers
  3. 通过索引(index,从 0 开始的整数,也称为下标)获取对应的元素,numbers[0]返回numbers的第 1 个元素。
  4. numbers 的第 2 个元素修改为9
  5. 再次打印numbers
  6. 打印numbers的长度(元素个数),结果为 3;
  7. 尝试打印numbers的第 4 个元素,遭遇RangeError,因为下标的最大值为 2(长度-1)。

常用构造函数

Dart 的 List 有多个实用的构造函数,现举例说明。

// ex632.dart
void main() {
  final empty = List<int>.empty(growable: true); // 1
  print(empty); // Output: []

  final squares = List.generate(
    10,
    (index) => index * index,
    growable: true,
  ); // 2
  print(squares); // Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

  final zeros = List.filled(5, 0, growable: false); // 3
  print(zeros); // Output: [0, 0, 0, 0, 0]

  final copy = List.of([3, 2, 1]); // 4
  print(copy); // Output: [3, 2, 1]
}
  1. List.empty 创建一个空的 List,命名参数growable用于指定创建的 List 是否可以增长,其默认值为false,表示创建一个固定长度(此处为 0)的 List;
  2. List.generate 的第一个参数指定待创建 List 的初始长度;第二个参数是一个函数(称为generator),其签名为E Function(int index)EList 的类型参数);
  3. List.filled使用指定的数据填充 List,其第一个参数为初始长度,第二个参数乃用于填充的数据;
  4. List.of从一个Iterable对象创建 List。

常用方法

如下示例展示了 List 的一些常用方法或属性以及对应说明。

// ex637.dart
void main() {
  var numbers = [for (var n = 0; n < 5; n++) n * n]; // 1
  print(numbers.runtimeType); // 1a Output: List<int>
  print(numbers.length); // 2 Output: 5
  print(numbers.first); // 3 Output: 0
  print(numbers[0]); // 3a Output: 0
  print(numbers.last); // 4 Output: 16
  print(numbers.isEmpty); // 5 Output: false
  print(numbers.isNotEmpty); // 6 Output: true

  print('-' * 9);
  print(numbers.sublist(1, 3)); // 7 Output: [1, 4]
  print(numbers.getRange(1, 3)); // 8 Output: (1, 4)
  print(numbers.reversed); // 9 Output: (16, 9, 4, 1, 0)
  print(numbers.followedBy([50, 60])); // 10 Output: (0, 1, 4, 9, 16, 50, 60)
  print(numbers.asMap()); // 11 Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

  print('+' * 9);
  print(numbers.where((a) => a % 2 == 0)); // 12 Output: (0, 4, 16)
  print(numbers.whereType<int>()); // 13 Output: (0, 1, 4, 9, 16)

  print(
    numbers.firstWhere((n) => n > 0 && n % 3 == 0, orElse: () => -1),
  ); // 14 Output: 9

  try {
    print(numbers.singleWhere((n) => n % 3 == 0, orElse: () => -1)); // 15
  } on StateError catch (e) {
    print(e.message); // 15a Output: Too many elements
  }

  print(numbers.map((n) => n + 1)); // 16 Output: (1, 2, 5, 10, 17)
  var result = numbers.fold<int>(5, (value, n) => value + n);
  print(result); // 17 Output: 35
  result = numbers.reduce((value, n) => value + n);
  print(result); // 18 Output: 30

  print('*' * 9);
  print(numbers.any((n) => n > 10)); // 19 Output: true
  print(numbers.every((n) => n > 0)); // 20 Output: false
  print(numbers.take(3)); // 21 Output: (0, 1, 4)
  print(numbers.takeWhile((n) => n % 2 == 1)); // 22 Output: ()
  print(numbers.skip(2)); // 23 Output: (4, 9, 16)
  print(numbers.skipWhile((n) => n % 2 == 1)); // 24 Output: (0, 1, 4, 9, 16)

  print('/' * 9);
  numbers.add(20); // 25
  numbers.addAll([30]); // 26
  numbers.removeAt(0); // 27
  numbers.insert(0, 1); // 28
  print(numbers); // 28a Output: [1, 1, 4, 9, 16, 20, 30]
  numbers.replaceRange(1, 3, [10, 40]); // 29
  print(numbers); // 29a Output: [1, 10, 40, 9, 16, 20, 30]
  numbers.shuffle(); // 30
  print(numbers); // 30a
  numbers.sort((a, b) => a - b); // 31
  print(numbers); // 31a Output: [1, 9, 10, 16, 20, 30, 40]
  numbers.clear(); // 32
  print(numbers.isEmpty); // 32a Output: true
}
  1. 构造一个名为 numbers 的 List,其值为 [0, 1, 4, 9, 16]
  2. length 返回 List 中元素的个数(即 List 的长度);
  3. first 返回 List 的第一个元素(element),如果 List 为空将引发StateError;另外可以通过下标 0 访问首个元素;
  4. last返回 List 的最后一个元素,如果 List 为空将引发StateError
  5. isEmpty 检查 List 是否为空;
  6. isNotEmpty 检查 List 是否非空;
  7. sublist(int start, [int? end]) 返回一个新的 List,它包含了原 List 指定区间([start, end),不含end)内的元素;
  8. getRange(int start, int end)sublist(start, end)类似,只不过它返回的是一个Iterable对象;
  9. reversed返回 List 的逆序的Iterable对象;
  10. followedBy(Iterable<E> other)返回一个 Iterable<E> 对象,它包含了该 List 和 other(追加部分)的所有元素;
  11. asMap()返回 List 的 Map 视图;
  12. where(bool test(E element))返回一个Iterable<E>对象,它使用一个test函数来过滤 List 的元素(e),通过测试(test(e)返回true)的那些元素将出现结果集中;
  13. whereType<T>()返回一个Iterable<T>对象,它筛选指定数据类型(T)的元素;
  14. firstWhere(bool test(E element), {E orElse()?})返回首个满足特定条件的元素,如果在 List 中找不到这样的元素,它就调用orElse函数并返回其值(如果 orElse!=null,否则将抛出StateError);
  15. singleWherefirstWhere类似,只不过当有多个元素满足测试条件时会抛出StateError
  16. map<T>(T toElement(E e))返回一个Iterable<T>对象,它使用toElement函数将 List 里的每个元素转换成另一个值;
  17. fold<T>(T initialValue, T combine(T previousValue, E element))遍历 List 并使用combine函数合并元素,最后返回一个单一的值,initialValue用于指定初始值;
  18. reduce(E combine(E value, E element))可被视为fold的特例,它使用 List 的首个元素作为initialValue,然后合并剩余的元素,因而reduce要求 List 非空,在空 List 上调用reduce方法将引起StateError;另外reduce的返回类型为E,而fold的返回类型为T(方法的类型参数);
  19. any(bool test(E element))检查 List 是否包含满足特定条件的元素;
  20. every(bool test(E element))检查是否 List 的每个元素都满足特定条件;
  21. take(int n)获取 List 的前n个元素;
  22. takeWhile(bool test(E value))返回 List 前面的元素(e),直到test(e)false
  23. skip(int n)跳过 List 的前n个元素并返回剩余部分;
  24. skipWhile(bool test(E value))跳过 List 前面的元素(e),直到test(e)false,并返回剩余部分;
  25. add(E value)向 List 中追加一个新元素(添加到末尾);
  26. addAll(Iterable<E> iterable)iterable里的所有元素追加到 List;
  27. removeAt(int index)将对应下标(index)的元素从 List 中移除;
  28. insert(int index, E e)将元素e插入到index对应的地方;
  29. replaceRange(int start, int end, Iterable<E> replacements)使用replacements中的元素替换 List 指定区间([start, end))的元素;
  30. shuffle([Random? random])将 List 中的元素随意打乱(洗牌);
  31. sort([int compare(E a, E b)?])对 List 中的元素排序,其顺序由compare函数指定;
  32. clear()清空 List。

关于 List 的更多细节,请参考官网文档https://api.dart.dev/dart-core/List-class.html

Set(集)

Set 是不重复元素的无序集,它主要关注元素是否在 Set 中,并具有两个特征:

  • 唯一性:其元素在 Set 内 是唯一的
  • 无序性:通常不能像 List 那样通过下标获取某个元素

对 Set 元素的遍历可以是无序,也可以是某方面有序的:

  • HashSet 是无序的
  • LinkedHashSet 按元素的写入顺序遍历
  • 有序 Set(如SplayTreeSet),按排序顺序进行遍历
classDiagram
  Set~E~ <|.. HashSet
  Set~E~ <|.. LinkedHashSet
  Set~E~ <|.. SetMixin~E~
  SetMixin~E~ <|-- SplayTreeSet~E~: with

  <<inferface>> Set
  <<final>> LinkedHashSet
  <<final>> HashSet
  <<final>> SplayTreeSet
  <<mixin>> SetMixin

Set 示例

先看一个简单的例子。

// ex641.dart

void main() {
  var fruites = {'Apple', 'Banana'}; // 1
  print(fruites.runtimeType); // Output: _Set<String>
  print(fruites); // Ouput: {Apple, Banana}

  fruites.add('Apple'); // 2
  print(fruites); // Ouput: {Apple, Banana}

  fruites.add('Cherry'); // 3
  print(fruites); // Ouput: {Apple, Banana, Cherry}

  print(fruites.length); // 4 Output: 3
}

  1. 使用字面量声明一个类型为Set 的变量 fruites ,它包含两个初始成员 'Apple' 和 'Banana';
  2. fruites添加一个成员Apple,由于Apple本来就已经在fruites Set中,因此这行代码执行后fruites的成员依旧只有原来的那两个;
  3. fruites追加一个新的成员Cherry,这样代码执行后,fruites一共包含了3个成员;
  4. fruites.length表示fruites的成员个数。

构造函数

Set 有如下工厂构造函数:

  • Set() 创建一个空的Set
  • Set.from(Iterable elements) 从一个Iterable对象创建Set,可理解为将一个Iterable对象转换为Set
  • Set.of(Iterable<E> elements)Set.from类似,区别在于前者返回的是Set<E>对象,即带有类型参数E
  • Set.identity() 创建一个空的 identity Set,该Set使用identical(Object?, Object?)函数对成员进行相等性测试
  • Set.unmodifiable(Iterable<E> elements)elements 创一个不可变Set

请阅读并分析下面的代码

// ex642.dart
import 'dart:math';

void main() {
  var list = [1, 2, 2, 3];

  var set1 = Set.from(list); // 1
  print('${set1.runtimeType} $set1'); // Output: _Set<dynamic> {1, 2, 3}

  var set2 = Set.of(list); // 2
  print('${set2.runtimeType} $set2'); // Output: _Set<int> {1, 2, 3}

  var a = Point(1, 2);
  var b = Point(1, 2);

  var set3 = Set.identity()
    ..add(a)
    ..add(b); // 3
  print(set3); // Output: {Point(1, 2), Point(1, 2)}

  var set4 = <dynamic>{}
    ..add(a)
    ..add(b); // 4
  print(set4); // Output: {Point(1, 2)}

  var set5 = Set.unmodifiable(set1);
  set5.add(5);
  // Unhandled exception:
  // Unsupported operation: Cannot change an unmodifiable set
}

Set特色方法

List一样,Set也实现了Iterable接口,即实现了Iterable接口定义的方法。由于在List章节中,我们已经对List的常用方法(包括Iterable接口中定义的方法)做了介绍,这里仅举例说明Set类型特有的方法。

// ex643.dart
void main() {
  var set1 = {1, 2, 3};
  var set2 = {2, 3, 4};

  var set3 = set1.union(set2); // 1
  print(set3); // Output: {1, 2, 3, 4}

  var set4 = set1.intersection(set2); // 2
  print(set4); // Output: {2, 3}

  var set5 = set1.difference(set2); // 3
  print(set5); // Output: {1}

  var set6 = set2.difference(set1); // 3a
  print(set6); // Output: {4}

  var a = set1.lookup(1); // 4
  print(a); // Output: 1

  var b = set1.lookup(4); // 4a
  print(b); // Output: null
}
  1. union(Set<E> other) 返回该Set与另一个Set(other)的并集;
  2. intersection(Set<Object?> other) 返回该Set与另一个Set(other)的交集;
  3. difference(Set<Object?> other) 返回该Set与另一个Set(other) 的差集,注意 this.difference(other)other.difference(this)是不同的;
  4. lookup(Object? object):如果Set中的某个成员与给定的对象object相等,就返回该成员,否则返回null

关于Set的更多方法,请查阅官方文档

Map(映射)

Map中的成员以“键值对”(key-value pair)的形式存在,Map也就是“键值对”的集合。同一个Map中 中的key不会重复,但不同的key可以关联到同一个value;通过key可以快速找到对应的value。理论上,Map的读写操作(查找、插入、删除)的平均时间复杂度都是O(1),即Map可以在一个常量时间内完成某个读或写操作,这个时间几乎不受Map内成员个数的影响。

flowchart TB
  subgraph abc [Map]
    subgraph ... 
    direction LR
    G(k...) --> H(v...)
    end

    subgraph entry2
    direction LR
    C(key2) --> D(value2)
    end
   
    subgraph entry1 
    direction LR
    A(key1) --> B(value1) 
    end
  end

key与value的数据类型乃参数化类型(泛型)。

abstract interface class Map<K, V> { 
  // ...
}

在稍后的示例ex651.dart中,变量 capitals的数据类型是Map<String, String>(key与value的类型均为String)。

示例

// ex651.dart
void main() {
  var capitals = {'US': 'Washington', 'UK': 'London', 'France': 'Paris'};
  print(capitals.runtimeType); // Output: _Map<String, String>

  print(capitals.entries); // 1
  // Outut: (MapEntry(US: Washington), MapEntry(UK: London), MapEntry(France: Paris))

  print(capitals.keys); // 1a Output: (US, UK, France)
  print(capitals.values); // 1b Output: (Washington, London, Paris)

  print(capitals['US']); // 2 Output: Washington
  print(capitals['Germany']); // 2a Output: null

  capitals['Germany'] = 'Berlin'; // 3
  capitals.putIfAbsent('Italy', () => 'Rome'); // 4
  print('${capitals['Germany']} ${capitals['Italy']}'); // Output: Berlin Rome

  var numbers = {'0xa': 10, '10': 10};
  print(numbers.values); // Output: 5 (10, 10)
  print(numbers.values.toSet()); // 5a Output: {10}
}
  1. 集合概述一节中已经提到,一个Map有3个Iterable对象(getter),即entries,keys 以及 values
  2. 类似于通过下标(从0开始的整数)获取List(数组)的成员,通过key来获取Map中与key对应的value(获取失败将返回null),这里的获取操作本即[]操作符方法;
  3. 与List类似,可通过[]=操作符方法,更新与key对应的value(若key不存在,则插入对应的key-value pair);
  4. putIfAbsent只有当key不存在时才更新对应的value;
  5. 请注意,values返回的是Iterable对象,不会自动去重,可通过values.toSet()获取values对应的Set。

构造函数

简单介绍一下的Map工厂构造函数:

  • Map() 创建一个空的 LinkedHashMap.
  • Map.from(Map other) 使用与other同样的 keys 与 values 创建一个 LinkedHashMap .
  • Map.fromEntries(Iterable<MapEntry<K, V>> entries) 从 entries 创建Map.
  • Map.fromIterable(Iterable iterable, {K key(dynamic element)?, V value(dynamic element)?}) 从一个Iterable对象创建Map,等价于<K, V>{for (var v in iterable) key(v): value(v)} .
  • Map.fromIterables(Iterable<K> keys, Iterable<V> values) 从给定的 keys 和 values 创建Map.
  • Map.identity() 创建一个 identity Map, 该Map使用identical(Object?, Object?)函数对key进行相等性测试.
  • Map.of(Map<K, V> other)other 创建一个LinkedHashMap
  • Map.unmodifiable(Map other)other 创建一个不可变Map.

练习

创建一个类型为Map<String,int>的Map,并准备100万(key-value pair)的测试数据,然后将测试数据逐个写入Map;在Map的长度由1万增长至100万的过程中,记录每插入100个pair的耗时;最后观察并分析这些耗时。

hashCode 与 ==操作符

Set 和 Map 通常依托哈希表实现。 逻辑上,哈希表可以抽象为一个两列的表格:一列是由 Object.hashCode生成的哈希码(即散列码),另一列则是对应的 entry(key-value pair)。物理存储上,程序先将 key 映射为哈希码,再将哈希码转换为数组索引,该索引决定了 Entry 在哈希表中的物理位置。通过这种方式,Set 和 Map 实现了极其高效的元素存取。有趣的是,在很多底层实现中,Set 其实就是一个“值为空”的 Map;例如 Go 语言并未提供内置的 Set 类型,而是建议开发者直接使用 Map。

flowchart LR
   A(( entry )) -->|key.hashCode| B(( 哈希码 ))
   B --> C(( 索引 ))
   C -->|构建| D[( 哈希表 )]
   D -->|查找| A

Set或Map通常基于Object.== 进行相等性测试。Object.hashCodeObject.== (equals)的默认实现仅体现了对象的标识符。为了保持Set或Map语义正确:

  • 两个相等的对象,其哈希值也必须相等;
  • 重写了Object.==也必须重写Object.hashCode,反之亦然;
  • 通常只对不可变类重写 Object.==

示例

下例中Point2类 重写了 Object.hashCodeObject.== ,而 Point类使用了其默认实现。

// ex661.dart
void main() {
  var a = Point(1, 2);
  var b = Point(1, 2);

  var c = Point2(1, 2);
  var d = Point2(1, 2);

  print('a.hashCode: ${a.hashCode}'); // 1
  print('b.hashCode: ${b.hashCode}'); // 1a

  print('c.hashCode: ${c.hashCode}'); // 2
  print('d.hashCode: ${d.hashCode}'); // 2a

  assert(a != b); // 3
  assert(c == d); // 3a

  var set1 = {a, b, c, d}; // 4
  print(set1); // Output: {Point(1, 2), Point(1, 2), Point2(1, 2)}

  var map1 = {a: 'a', b: 'b', c: 'c', d: 'd'}; // 5
  print(map1); // Output: {Point(1, 2): a, Point(1, 2): b, Point2(1, 2): d}
}

class Point {
  final int x, y;
  Point(this.x, this.y);

  @override
  String toString() => 'Point($x, $y)';
}

class Point2 {
  final int x, y;
  Point2(this.x, this.y);

  @override
  String toString() => 'Point2($x, $y)';

  @override
  int get hashCode {
    const prime = 31;
    var result = 1;
    result = prime * result + x.hashCode;
    result = prime * result + y.hashCode;
    return result;
  }

  @override
  bool operator ==(Object other) {
    if (other is Point2) {
      return x == other.x && y == other.y;
    }
    return false;
  }
}
  1. 变量a,b的类型为Point,虽然它们的成员变量(xy)的值都一样,但它们的哈希值不同(原因是它们的对象标识不同);
  2. 变量c,d的类型为Point2,它们的哈希值相同(因为我们重写了Object.hashCodeObject.==,只要它们的成员变量具有相同的值,它们的哈希值就一定相同);
  3. a != bc == d,显然成立;
  4. 由于 c==d 成立, set1 只有3个元素;
  5. 同理 map1只有3个entries,map1内部将cd视为同样的key(即Point2(1, 2)),与其对应的value是最新的那个值'd'

异步编程概述

异步编程之于 Dart,正如多线程之于 Java。在 Java 世界中,多线程是处理并发任务的核心;而在 Dart 的世界里,异步编程承担着同样的角色;它是程序在处理耗时操作(如网络请求、数据库操作、文件读写)时,界面依然能够灵敏响应、不卡顿的基石。

下面的示例模拟了用户在登录时,程序让用户无助等待了 5 秒的场景。

// ex711.dart
import 'dart:io';

void main() {
  var watch = Stopwatch()..start();

  var ok = login('userid', 'passwd'); // 1
  if (ok) {
    print('Welecome!');
  } else {
    print('try again');
  }

  print(watch.elapsedMilliseconds);
}

bool login(String userId, String passwd) {
  // Simulate a long task that will take 5 seconds
  sleep(Duration(seconds: 5));
  return true;
}

  1. 同步调用login函数,程序执行流程被阻塞(block,此处由sleep引起),直到login函数返回;
  2. 这里模拟了login函数花了5秒钟完成用户名和密码的校验。

核心机制:Event Loop 与它的“三剑客”

Dart 采用的是单线程模型,其异步并非通过多线程并行实现,而是基于 Event Loop(事件循环) 机制。

你可以把 Event Loop 想象成一个超级服务员:当他下单给厨师炒菜(耗时操作)后,并不会在取餐口原地干等,而是利用这段空档先去给其他客人倒水、点菜。他绝不让自己闲着,也绝不让任何一位客人干等。这种灵活的调度,正是 Dart 能够以单线程实现高并发响应的奥秘。

除了 Event Loop,要掌握 Dart 异步编程,我们还必须理解下面三个核心概念:

  • Future: 代表一个“承诺”,即在未来某个时刻会交付的结果(一次性任务)。
  • Stream: 代表一连串“流动”的数据,像自来水管一样持续传输事件(持续性任务)。
  • Isolate: 类似于轻量级进程。它拥有独立的内存空间,Isolate 之间互不共享内存,仅靠消息通信。它通常用于处理最吃性能的 CPU 密集型计算任务。

本章将带你由浅入深,先掌握最常用的 Future 与 Stream,随后揭开 Event Loop 与 Isolate 的底层面纱。在本章的最后,我们将通过对工业级 WorkerPool 技术的深度剖析,帮助你构建起对 Dart 异步并发体系的完整认知。

Future

Future 代表了一个“在未来某个时间点”才会完成的操作结果。 Future 的生命周期可分为两个阶段:

  • 未完成 (Uncompleted):异步操作正在进行中,结果尚未产生;
  • 已完成 (Completed):操作结束,会有两种可能的结果:
    • 成功 (Value):操作顺利完成,返回具体的数据(如 Future 返回字符串);
    • 失败 (Error):操作中途出错,返回一个异常。
stateDiagram
    [*] --> Uncompleted : Create Future
   
    Uncompleted --> Success : Value
    Uncompleted --> Failure : Error

    Success --> [*] : .then / await
    Failure --> [*] : .catchError / try-catch

示例

先通过一个例子来看同步函数与异步函数及其使用上的区别:

// ex721.dart
import 'dart:async';
import 'dart:io';

void main() {
  final watch = Stopwatch()..start(); // 3
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o'); // 3a

  myprint('before compute');
  var result = compute(3, 4); // 4
  myprint('after compute');
  myprint('result=$result');

  myprint('before computeAsync');
  var futureResult = computeAsync(5, 12); // 5
  myprint('after computeAsync');

  futureResult.then((onValue) => myprint('result=$onValue')); // 6

  /* An example output:
out0 0ms: before compute
out1 1004ms: after compute
out2 1005ms: result=7
out3 1005ms: before computeAsync
out4 1010ms: after computeAsync
out5 2020ms: result=17
*/
}

// 1
int compute(int a, int b) {
  // Simulate a long task
  sleep(Duration(seconds: 1));
  return a + b;
}

// 2
Future<int> computeAsync(int a, int b) {
  return Future(() => compute(a, b));
}
  1. compute 是一个同步函数,它模拟了一个耗时约1秒种的计算任务(根据ab进行计算),然后返回其结果;
  2. computeAsynccompute的异步版本,它是一个异步函数,它使用Future.new创建了一个Future对象并返回;
  3. Stopwatch是一个用于计时对秒表,精确到微秒(microseconds),该示例使用了毫秒(milliseconds)作为计时单位(3a);
  4. 调用compute函数,程序阻塞直到compute返回结果(out0 - out2);
  5. 调用computeAsync函数得到的是一个Future对象,computeAsync函数返回时,真正的计算(compute)还在进行中,即程序不会挂起等待计算结果(out3 - out4);
  6. futureResult.then提供一个callback(回调)函数,当computeAsync内部计算完成时,callback将被调用(out5)。

同步 vs 异步:

特性同步函数 (compute)异步函数 (computeAsync)
定义方式int compute(...)Future<int> computeAsync(...)
返回值直接返回结果返回一个“凭证” Future,承诺以后给结果
执行表现调用时程序会“停”在这一行直到算完调用后程序可以立即去做别的事
调用方式var res = compute(3, 4);computeAsync(3, 4).then(...);

async/wait

上述示例中的computeAsync可以用asyc/wait改写,这是更现代、更具可读性的风格,让异步代码看起来像同步代码。

// ex722.dart
void main() async { // 3
  var result = await compute(8, 15); // 2
  print(result); // Output: 23
}

// 1
Future<int> compute(int a, int b) async => a + b;
  1. async写在函数(或方法)签名的最后,这里标记compute为异步函数;
  2. 调用异步函数compute时,使用前缀关键字await,使得异步代码像同步代码那样简洁;
  3. await 关键字只能在异步函数(或方法)中。

异常处理

下面的例子演示了有关Future的异常处理。

// ex723.dart
void main() async {
  divide(2, 1).then((val) => print(val)); // Output: 2

  divide(2, 0).then((val) => print(val)).catchError((e) => print(e));
  // Output: IntegerDivisionByZeroException

  print(await divide(3, 1)); // Output: 3

  try {
    print(await divide(3, 0));
  } on Exception catch (e) {
    print(e); // Output: IntegerDivisionByZeroException
  }
}

Future<int> divide(int a, int b) async => a ~/ b;

构造函数

常用的 Future 构造函数:

  • Futrue(computation)Futrue.new) 使用 Timer.run 创建一个包含computation()的计算结果的Future(异步执行)
  • Futrue.sync(computation) 创建一个包含computation()的计算结果的Future(同步执行);
  • Future.delayed(duration,[computation]) 延迟一段时间后执行代码(常用于模拟网络延迟)
  • Future.value([value]) 立即创建一个成功状态的 Future
  • Future.error(error, [stackTrace]) 立即创建一个失败状态的 Future
  • Future.microtask(computation) 将任务放入微任务队列(Microtask Queue),相比Futrue.new优先级更高

请仔细阅读并分析如下示例。

// ex724.dart
void main() {
  final watch = Stopwatch()..start();
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

  myprint('start');
  Future.delayed(Duration(seconds: 1), () => myprint('deplay'));
  Future.microtask(() => 'microtask1').then(myprint);
  Future(() => 'new').then(myprint);
  Future.sync(() => 'sync').then(myprint);
  Future.value('value').then(myprint);
  Future.error('error').catchError(myprint);
  Future.microtask(() => 'microtask2').then(myprint);

  /* An example output:
out0 0ms: start
out1 24ms: microtask1
out2 24ms: sync
out3 25ms: value
out4 27ms: error
out5 28ms: microtask2
out6 30ms: new
out7 1014ms: deplay
*/
}

实用方法

Future提供了下列实用的静态方法:

  • Future.wait(futures,{...}) 同时启动多个Future,并等待它们全部完成;
  • Future.any(futures) 谁先完成就返回谁(无论是成功还是失败);
  • Future.doWhile(action) 重复执行异步操作,直到返回 false 为止。
  • Future.forEach(elements, action) 对集合中的每个元素顺序执行异步操作,它会等前一个执行完再开始下一个。

下面的示例代码不难理解,请读者自行分析。

// ex725.dart
import 'dart:math';

void main() {
  final watch = Stopwatch()..start();
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

  Future.wait([
    Future(() => myprint('wait1')),
    Future(() => myprint('wait2')),
    Future(() => myprint('wait3')),
  ]);

  Future.wait([
    Future(() => 1),
    Future(() => 2),
    Future(() => 3),
  ]).then(myprint);

  Future.any([
    Future.delayed(Duration(milliseconds: 100), () => myprint('any1')),
    Future.delayed(Duration(milliseconds: 100), () => myprint('any2')),
    Future.delayed(Duration(milliseconds: 100), () => myprint('any3')),
  ]);

  Future.any([
    Future.delayed(Duration(milliseconds: 100), () => 4),
    Future.delayed(Duration(milliseconds: 100), () => 5),
    Future.delayed(Duration(milliseconds: 100), () => 6),
  ]).then(myprint);

  final rand = Random();
  Future.doWhile(() {
    var d = rand.nextDouble();
    myprint(d);
    return d < 0.8;
  });

  Future.forEach(['A', 'B', 'C'], myprint);

  /* An example output:
out0 13ms: 0.06789414914959846
out1 15ms: 0.142393000844255
out2 15ms: 0.7807443343674055
out3 15ms: 0.2542896840253688
out4 15ms: 0.5590822258793036
out5 15ms: 0.8071049146003575
out6 17ms: A
out7 17ms: B
out8 17ms: C
out9 19ms: wait1
out10 21ms: wait2
out11 21ms: wait3
out12 24ms: [1, 2, 3]
out13 110ms: any1
out14 113ms: any2
out15 113ms: any3
out16 114ms: 4
   */
}

Timeout(超时)

Future.timeout()实例方法是一个实用的守时工具。它允许我们为一个异步操作设置一个最大容忍时间(timeLimit)。简单来说,它的逻辑是:“要么在规定时间内给我结果,要么我就抛出异常走人。”

// ex726.dart
void main() async {
  var data = await fetchData().timeout(Duration(milliseconds: 2000)); // 1
  print(data); // Output: ok

  data = await fetchData().timeout(Duration(milliseconds: 500)); // 2
  print(data);
  /* Output:
Unhandled exception:
TimeoutException after 0:00:00.500000: Future not completed
*/
}

Future<String> fetchData() async {
  // Simulate a long task
  await Future.delayed(Duration(seconds: 1));
  return 'ok';
}
  1. fetchData()函数在 timeLimit(2s) 到达之前完成了,timeout()方法返回的 Future 会正常返回原任务的结果,就好像 timeout() 从未存在过一样。
  2. timeLimit(0.5s)耗尽时fetchData()函数还没执行完,于是这一行代码抛出了TimeoutException

通过给timeout()提供 onTimeout 参数 ,可以避免遭遇TimeoutExceptiononTimeout 函数返回一个超时的保底值。

// ex727.dart
void main() async {
  var data = await fetchData().timeout(
    Duration(milliseconds: 500),
    onTimeout: () => 'timeout',
  );
  print(data); // Output: timeout
}

Future<String> fetchData() async {
  // ... (omitted for brevity)
}

Reference

Stream(流)

如果说 Future 是“一次性买卖”,那么 Stream 就是“订阅服务”。Stream 是一系列异步事件的序列。它会随着时间的推移发出多个值,直到任务完成或发生错误。

graph LR
    A((生成数据)) --> B@{ shape: das, label: "Stream" }
    B -->|数据事件 Data| C([监听器 Listener])
    B -->|错误事件 Error| C
    B -->|完成事件 Done| C
    C --> D{处理数据}

Stream 是异步版的Iterable,借助下表我们可以快速掌握其核心差异。

单个数据多个数据
同步T (普通变量)Iterable<T>
异步Future<T>Stream<T>

Iterable 与 Stream 的对称性:

  • 数据结构对称性:Iterable 允许你遍历一组已经存在的数据;Stream 允许你“遍历”一组未来才会到达的数据。
  • 处理逻辑对称性:在 Iterable 上用的 mapwhereexpand,在 Stream 中几乎完全通用。
  • 语法对称性:Iterable 对应 sync* / yield,Stream 对应 async* / yield

示例

下面的示例演示了如何Stream API创建和监听Stream。

// ex731.dart
import 'dart:async';
import 'dart:math';

void main() async {
  final watch = Stopwatch()..start();
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

  randomStream(5).listen(myprint, onDone: () => myprint('done')); // 5

  /** An example output:
out0 119ms: 1
out1 212ms: 4
out2 312ms: 2
out3 415ms: 2
out4 514ms: 4
out5 518ms: done
   */
}

// 1
Stream<int> randomStream(int max) {
  var controller = StreamController<int>(); // 2
  // Generate random int asynchronously
  final rand = Random();
  for (var i = 1; i <= max; i++) {
    Future.delayed(Duration(milliseconds: 100 * i), () {
      controller.sink.add(rand.nextInt(max)); // 3
      if (i == max) {
        controller.close(); // 4
      }
    });
  }
  return controller.stream; // 2a
}
  1. randomStream(max) 是数据的生产者,它每间隔100毫秒产生一个[0, max)区间内的随机整数(异步执行);
  2. 使用StreamController来控制数据的生成,在函数的最后返回 controller.stream
  3. 调用 controller.sink.add(event)(等同于controller.add(event))将数据放进controller.stream,由于该操作时延迟且异步进行的,当randomStream函数返回之后才会执行;
  4. 生成max个数据后关闭Stream;
  5. 调用 Stream.listen(onData,{...})方法监听Stream并消费(处理)数据。

Iterable 生成器( sync*-yield

在介绍异步生成器 async*-yield 之前,先来看下Iterable生成器(使用 sync*-yield 创建 Iterable 对象),请对两者进行对比。

// ex732.dart
void main() {
  print(countList(5)); // 1 Output: (1, 2, 3, 4, 5)
  print(countList2(5)); // 2 Output: [1, 2, 3, 4, 5]
}

Iterable<int> countList(int max) sync* {
  for (int n = 1; n <= max; n++) {
    yield n;
  }
}

Iterable<int> countList2(int max) => [for (int n = 1; n <= max; n++) n];

async* / await for

与 Future 的 async / await 语法对应,Stream提供了async* / await for语法;使用该语法改写示例ex731,我们得到了一个更为现代与简洁的版本:

// ex733.dart
import 'dart:math';

void main() async { // 4
  final watch = Stopwatch()..start();
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

  // 3
  await for (var n in randomStream(5)) {
    myprint(n);
  }
  myprint('done');
}

// 1
Stream<int> randomStream(int max) async* {
  final rand = Random();
  for (var i = 0; i < max; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield rand.nextInt(max); // 2
  }
}
  1. async* 写在函数签名的最后,用以标记该函数是一个异步生成器(返回一个 Stream 对象);
  2. 使用 yield 将生成的数据放进Stream,此类函数不用return
  3. 使用 await for...in... 监听并消费Stream里的数据;
  4. 使用 await 关键字的外围函数必须带 async 标记;

广播 (Broadcast)

以上介绍的是 Stream 的“单订阅”(Single-subscription) 模式。然而,Stream 还有另一种更为灵活的形态,即广播模式(Broadcast)。我们先来看下两种模式的区别。

特性单订阅广播
监听数量只能被 listen 一次,第二次报错可以有无限个监听者同时监听
缓存机制监听前的事件可能会在缓冲区等待事件是即时的,错过监听时间点就拿不到旧数据
典型用途文件读取、网络请求(端对端数据流)状态管理、传感器数据、UI 事件通知

请看下面的示例:

// ex734.dart
import 'dart:async';

void main() {
  final watch = Stopwatch()..start();
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

  var datas = countStream(5);
  datas.listen((d) => myprint('sub1 $d')); // 1

  // 0.2s later
  Future.delayed(
    Duration(milliseconds: 200),
    () => datas.listen((d) => myprint('sub2 $d')), // 2
  );

  /* An example output:
out0 128ms: sub1 1
out1 210ms: sub1 2
out2 323ms: sub1 3
out3 323ms: sub2 3
out4 415ms: sub1 4
out5 415ms: sub2 4
out6 510ms: sub1 5
out7 510ms: sub2 5
   */
}

Stream<int> countStream(int max) {
  var controller = StreamController<int>.broadcast(); // 1
  for (var n = 1; n <= max; n++) {
    Future.delayed(Duration(milliseconds: 100 * n), () => controller.add(n));
  }
  return controller.stream;
}
  1. StreamController.broadcast 构造函数返回一个广播Stream;
  2. 第一个消费者(sub1)监听并消费数据;
  3. 0.2秒之后,第二个消费者(sub2)监听并消费数据;注意观察程序输出,对比两个消费者各自监听到的数据。

可以用 async*-yield 语法改写此示例的 countStream 函数如下:

Stream<int> countStream(int max) async* {
  for (var n = 1; n <= max; n++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield n;
  }
}

改写后的countStream 函数更加简洁。相应地 var datas = countStream(5); 一行需改写为:

    var datas = countStream(5).asBroadcastStream(); // 1 

Stream 的常用方法

订阅与生命周期

  • listen(): 最核心的方法。手动订阅流,处理 onData, onError, onDone 事件。
  • await for: 异步迭代器。像遍历 List 一样消费流(最简洁的消费方式)。
  • asBroadcastStream(): 将单订阅流转换为广播流(多订阅流)。
  • timeout(duration): 设置超时时间,若规定时间内无事件则触发错误或执行回调。

转换

这些方法返回一个新的 Stream,且大多是惰性的。

  • map<T>(convert): 将流中的每个元素同步转换为另一种类型。
  • asyncMap<T>(convert): 异步转换。等待 Future 完成后再发射下一个元素。
  • expand(convert): 将一个元素转换为多个元素,并平铺(Flatten)到流中。
  • asyncExpand(convert): 将每个元素转换为一个新的 Stream 并按顺序连接。
  • transform(transformer): 使用 StreamTransformer 进行自定义的复杂转换(如解密、编码)。

过滤

  • where(test): 筛选掉不满足条件的元素。
  • distinct([equals]): 过滤掉与前一个元素连续重复的元素。
  • take(count): 只获取前 count 个元素。
  • takeWhile(test): 持续获取,直到条件不再满足。
  • skip(count): 跳过前 count 个元素。
  • skipWhile(test): 跳过直到条件不再满足,之后开始获取。
  • handleError(onError): 捕获并处理流中的错误,防止流中断。

聚合与检索

  • toList(): 将流中所有元素收集到一个 List 中。
  • toSet(): 将流中所有元素收集到一个 Set 中。
  • fold<T>(initial, combine): 提供初始值,将元素累加合并为单一结果。
  • reduce(combine): 无初始值,两两合并流中的元素(要求非空)。
  • join([separator]): 将元素转为字符串并用分隔符连接。
  • first / last / single: 获取流中第一个、最后一个或唯一一个元素。
  • any(test) / every(test): 检查是否至少一个/全部元素满足条件。
  • contains(element): 检查流中是否包含特定元素。

一个简单示例:

// ex736.dart
void main() async {
  var nums = Stream.fromIterable([for (var i = 0; i < 10; i++) i]);
  print(await nums.join("-")); // Output: 0-1-2-3-4-5-6-7-8-9
  print(await nums.map((i) => i * i).toList()); // 1*1,2*2,...
  print(await nums.where((i) => i % 2 == 0).toList()); // even numbers
  print(await nums.fold<int>(0, (old, i) => old + i)); // 1+2+...
  print(await nums.contains(1)); // Output: true

  print(
    await nums
        .take(5)
        .map((i) => i * 3 + 1)
        .expand((i) => [i, i * i, i * i * i])
        .where((i) => i < 200)
        .toList(),
  );
  // Output: [1, 1, 1, 4, 16, 64, 7, 49, 10, 100, 13, 169]
}

yield*(yield-each)

yield* (yield-each)是Dart 非常强大的异步生成器。如果说 yield 是插入一个元素,那么 yield* 就是插入一个 Stream 中的所有元素。yield* 后接 一个 Stream,其作用是暂停当前的生成器,转而监听后面的 Stream,并将其产生的所有事件逐一转发,直到那个 Stream 结束,才继续执行当前生成器接下来的代码。

先来看一个使用 yield* 的例子:

// ex737.dart
void main() async {
  var nums = combinedStream(Stream.fromIterable([1, 2, 3]));
  print(await nums.toList()); // Output: [1, 2, 3, 0]
}

Stream<int> combinedStream(Stream<int> prev) async* {
  // 1
  await for (final n in prev) {
    yield n; 
  }
  yield 0; // 2
}

combinedStream 函数使用 await forprev 中的元素一个个写进目标stream后,然后追加一条数据(0)。用 yield* 改写该函数如下:

// ex738.dart
Stream<int> combinedStream(Stream<int> prev) async* {
  yield* prev; // 1
  yield 0; // 2
}

改写后的函数看起来简洁多了。

异步递归

文件夹是一个典型的递归结构(文件夹内包含文件夹)。使用 yield* 可以让你像写同步递归一样,源源不断地产出深度嵌套的文件。

// ex739.dart
import 'dart:io';

void main() {
  final dir = Directory('.');
  traversal(dir).listen((f) => print(f.path));
}

Stream<File> traversal(Directory dir) async* {
  final entities = await dir.list().toList(); // 1
  for (var entity in entities) {
    if (entity is File) {
      yield entity; // 2
    } else if (entity is Directory) {
      yield* traversal(entity); // 3
    }
  }
}
  1. 获取 dir 的子目录及文件;
  2. 如果 entity 是文件(File),就直接产出(yield);
  3. 递归调用并将子目录流的所有结果“委派”给当前流(yield*)。

如果不用 yield*,就必须使用 await for 循环去解包子流,代码会变得冗长且难以维护。 在处理树形结构的数据时,yield* 是实现异步递归的唯一标准方式。

yieldyield* 对比

特性yieldyield*
右侧对象单个数据(如 int, String一个 Stream<T>
行为产出一个值后继续代理另一个Stream,直到其结束才继续
性能逐个处理更加高效(Dart 引擎对其有内部优化)

关键特性与陷阱

执行顺序

yield* 会挂起(suspend)当前生成器的执行。只有当 yield* 后面的Stream关闭(close)后,当前函数才会执行下一行。

错误传递

如果 yield* 监听的Stream抛出了异常,这个异常会直接传递到当前Stream中,除非你在外层用了 try-catch

广播流

如果 yield* 后面是一个广播流,那么它只会转发从“订阅那一刻”开始的数据。

Isolate 与 Event Loop

本节将简单介绍 Isolate 与 Event Loop。下一节将详细介绍如何使用 Isolate。

并发模型

不同于 Java 基于共享内存的多线程并发,Dart 采用了名为 Isolate 的隔离并发模型,其运行机制类似于轻量级进程。Isolate 之间采用“无共享”(Shared-Nothing)架构,通信完全依赖于异步消息传递。这种天然的隔离机制从根本上消除了数据竞争(Data Race),无需引入复杂的锁机制(Locks)即可确保运行时的内存安全。

graph TD
    subgraph "Process (Java, C++) - 共享内存模型"
        M[(Shared Heap Memory)]
        T1([Thread 1]) --- M
        T2([Thread 2]) --- M
        T3([Thread 3]) --- M
        style M fill:#f96,stroke:#333,stroke-width:2px
    end

    subgraph "Process (Dart) - 隔离模型"
        subgraph I1 [Isolate 1]
            H1[(Heap)] --- E1([Event Loop])
        end
        subgraph I2 [Isolate 2]
            H2[(Heap)] --- E2([Event Loop])
        end
        I1 -.->|Message Passing| I2
        style H1 fill:#6cf,stroke:#333
        style H2 fill:#6cf,stroke:#333
    end

Isolate 这种设计让 Dart 能够实现 “无停顿垃圾回收”。因为每个 Isolate 的 GC 是独立的,当子 Isolate 在后台清理垃圾时,不会阻塞主 Isolate 的 UI 渲染,这正是 Flutter 保持流畅(60/120 FPS)的底层秘诀之一。

Isolate

每一个 Isolate 到底初始化了什么? 当你 spawn 一个新的 Isolate 时,Dart VM 实际上做了以下工作:

  • 分配独立的堆内存:用于存放该 Isolate 产生的对象。
  • 创建Event Loop(事件循环):每个 Isolate 都有自己的事件队列和调度器。
  • 创建调用栈(Stack):独立的执行上下文。
  • 独立的垃圾回收器(Private GC):Isolate A 在进行 GC 时,不会停止 Isolate B 的运行(即没有全系统的 Stop-the-world)。
graph TD
    %% 全局容器
    subgraph Isolate_Internal ["Dart Isolate Instance"]
        direction TB

        %% 内存空间
        subgraph Memory_Space ["私有内存堆 (Private Heap)"]
            Objects([对象/实例])
            GC[垃圾回收器]
        end

        %% 事件驱动核心
        subgraph Event_System ["事件处理系统 (Event Loop System)"]
            MQ@{shape: das, label: "微任务队列 Microtask Queue" }
            EQ@{shape: das, label: "事件队列 Event Queue" }
            Loop((⚙️ 事件循环 Event Loop))
        end

        %% 运行栈
        Stack[[调用栈 Stack]]

        %% 外部接口
        subgraph Ports ["通信网关 (Communication)"]
            RP[ReceivePort 接收端口]
            SP[SendPort 发送端口]
        end
    end

    %% 连接逻辑
    Loop --> MQ
    MQ -->|处理| Stack
    Loop --> EQ
    EQ -->|处理| Stack
    Stack -->|修改/读取| Objects
    
    %% 通信逻辑
    External((外部其他 Isolate)) <-.->|消息传递/数据拷贝| Ports
    RP -->|触发事件| EQ

    %% 样式美化
    style Isolate_Internal fill:#f5f5f5,stroke:#333,stroke-width:2px
    style Memory_Space fill:#e3f2fd,stroke:#1565c0
    style Event_System fill:#fff3e0,stroke:#e65100
    style Ports fill:#f1f8e9,stroke:#33691e
    style Loop fill:#673AB7,stroke:#311B92,stroke-width:2px,color:#fff
    style MQ fill:#FFF9C4,stroke:#FBC02D
    style EQ fill:#E1F5FE,stroke:#0288D1
    style Stack fill:#4CAF50,stroke:#1B5E20,color:#fff

Isolate 生命周期

在 Dart 中,Isolate 的生命周期是一个闭环的异步过程。由于 Isolate 之间不共享内存,其状态切换高度依赖于 事件循环(Event Loop)通信端口(Port)

stateDiagram-v2
    [*] --> Spawning: Isolate.spawn()
    
    state Spawning {
        [*] --> Allocating: 分配独立堆内存
        Allocating --> Loading: 加载代码与入口函数
    }

    Spawning --> Running: entryPoint(入口函数)开始执行
    
    state Running {
        [*] --> EventLoop
        EventLoop --> MicrotaskQueue: 处理微任务
        MicrotaskQueue --> EventQueue: 处理事件/消息
        EventQueue --> EventLoop: 循环往复
        
        state "Active (Listening)" as Active
        EventLoop --> Active: ReceivePort 开启
        Active --> EventLoop: 收到消息
    }

    Running --> Exiting: entryPoint执行完毕 && 无活跃端口
    Running --> Exiting: 收到 kill 信号
    Running --> ErrorState: 未捕获的异常
    
    ErrorState --> Exiting: 触发 ErrorListener
    Exiting --> [*]: 资源回收/内存释放

Event Loop

Isolate与Event Loop是密不可分的,每个Isolate都有唯一的一个Event Loop与之对应。Isolate 是物理边界,而 Event Loop 是运行逻辑。

graph TD
    subgraph Core [" "]
        direction TB
        EL((⚙️ Event Loop))
    end

    MTQ@{shape: das, label: "Microtask Queue" }
    EQ@{shape: das, label: "Event Queue" }

    Exec[[当前执行上下文]]

    EL ==> |轮询并清空| MTQ
    MTQ -->|弹出任务| Exec
    Exec -.->| microtask | MTQ
    Exec -.->| event | EQ    
    
    EL ==>|检查并取出一个| EQ
    EQ -->|弹出任务| Exec

    Input([外部事件: I/O, Click, Timer]) ---> |进入队列| EQ

    style EL fill:#673AB7,stroke:#311B92,stroke-width:4px,color:#fff
    style Exec fill:#4CAF50,stroke:#1B5E20,color:#fff
    style MTQ fill:#FFF9C4,stroke:#FBC02D
    style EQ fill:#E1F5FE,stroke:#0288D1
    style Core fill:none,stroke:none

Event Loop就像一个不停转动的时间齿轮, 同时维护着两个优先级不同的队列:

  • Event Queue(事件队列):优先级较低。包含外部事件,如 I/O、计时器(Timer)、鼠标点击、绘制事件等。

  • Microtask Queue(微任务队列):优先级最高。通常用于非常简短的、需要异步执行的操作。只要微任务队列不为空,Event Loop 就会一直执行它,直到清空。 注意:如果在微任务中不断产生新的微任务,事件队列就会被“饿死”,导致 UI 无法响应。

Event Queue 保证了程序的响应性,它让 UI 刷新和用户点击有条不紊地排队。而 Microtask Queue 则保证了异步逻辑的原子性。当一个 Future 完成时,我们希望它的后续处理(then)能尽快执行,甚至赶在下一个用户点击之前。微任务队列就是为了这种‘插队’需求而生的,它让异步逻辑在宏观上看起来像是连续执行的。

再谈 asyc/wait

在 Dart 中,async/await 并不是让代码进入了另一个线程,而是将一个长函数拆分成了多个逻辑片段,并利用 Event Loop 重新排队。下面结合示例进行分析。

// ex741.dart
void main() async {
  print('main start');
  var result = await task();
  print(result);
  print('main end');

  /* Output:
main start
task start
task done
main end
  */
}

Future<String> task() async {
  await Future(() => print('task start'));
  return 'task done';
}
sequenceDiagram
    autonumber
    participant M as main
    participant EL as ⚙️ Event Loop
    participant MTQ as Microtask Queue (MTQ)
    participant EQ as Event Queue
    participant Exec as Execution Context

    Note over M, Exec: 同步启动阶段
    M->>Exec: print('main start')
    M->>Exec: 调用 task()
    
    Note right of Exec: 进入 task 内部
    Exec->>EQ: 注册 Future 任务 (print 'task start')
    Exec-->>M: task 函数挂起 (await Future)
    
    Note over M, Exec: 控制权回归 main
    M-->>EL: main 函数挂起 (await task)
    
    Note over EL: 齿轮旋转 - 处理宏任务
    EL->>EQ: 提取并执行 Future
    EQ->>Exec: print('task start')
    Exec->>MTQ: Future 完成,恢复 task 剩余部分
    
    Note over EL: 齿轮旋转 - 优先调度 (MTQ)
    EL->>MTQ: 提取任务:完成 task 并返回 'task done'
    MTQ->>Exec: 执行 return 'task done'
    Exec->>MTQ: task 完成,恢复 main 剩余部分
    
    Note over EL: 再次清空微任务
    EL->>MTQ: 提取任务:打印结果
    MTQ->>Exec: print('task done')
    MTQ->>Exec: print('main end')
    
    Note over Exec: 所有任务链结束

上述时序图中的三个关键“跳转”:

  • a. 在 task() 中,一遇到 await Future(...)task 函数就会立即交出控制权。此时并没有立即出现 task start,因为 task start 被丢进了 Event Queue。

  • b. 双重挂起: 此时内存中有两个处于“暂停”状态的函数:main 在等 tasktask 在等 Future。这种嵌套挂起展示了 Dart 异步非阻塞的本质。

  • c. 微任务的“接力”: 当 Future(宏任务)执行完后,它并没有直接回到 main。它先触发了 task 的恢复(微任务),task 完成后又触发了 main 的恢复(又一个微任务)。这就是为什么 Microtask Queue 被称为异步链条的“粘合剂”。

sleep() VS await Future.delayed()

sleep()是一个同步阻塞(block)调用,它将强行卡死EventLoop时间齿轮,霸占 CPU 并原地停止。

await Future.delayed(),是一个异步非阻塞操作,就像执行区里的工人看了一眼闹钟,发现时间没到,于是主动离开位子,把执行区让出来,自己去休息室等候。其过程是:

  • Future.delayed 向底层操作系统注册一个计时器
  • await 关键字将当前函数挂起(Suspend)
  • 执行区立即变空,Event Loop 齿轮可以自由转动,去处理屏幕刷新、点击等其他队列任务。
  • 时间一到,计时器把“恢复执行”的任务丢进 Event Queue,等齿轮下一轮转过来时执行。
特性sleep(...)await Future.delayed(...)
执行性质同步、阻塞异步、挂起(非阻塞)
对EventLoop齿轮影响停止转动正常转动
队列任务被饿死(Starvation)正常调度
用户体验应用卡死,无法操作应用流畅,后台等待
底层实现操作系统线程休眠计时器事件注册 + 任务回流

Async 还是 Isolate ?

虽然 async/await 能处理异步,但它本质上还是在同一个线程的 Event Loop 里“排队”。如果你需要处理 CPU 密集型任务(如大图片滤镜、复杂加解密、解析 100MB 的 JSON),主线程依然会卡顿,这时就需要启动一个新的 Isolate 进行并行处理。

场景推荐方案底层逻辑
网络请求 (HTTP)async/await线程在等待 IO 响应,不需要额外 CPU 计算。
文件读写 (I/O)async/await由操作系统异步处理,返回后在 Event Loop 排队。
JSON 解析 (巨大)Isolate解析需要消耗大量 CPU 时间,会阻塞 UI 渲染。
图像/视频处理Isolate属于高强度计算,必须移出 Main Isolate。

Reference

Isolate 基础应用篇

按使用场景(是简单的计算还是长期的后台任务)的不同,Isolate有三种主流的创建方式:

  • Isolate.run()(单次轻量):Dart会自动处理 Isolate 的创建、参数传递、结果返回以及最后的销毁
  • Isolate.spawn()(常驻/双向通信):如果你需要一个常驻的后台服务
  • Flutter 特色:compute(),类似Isolate.run(),更加兼容 Flutter 环境

此外,Isolate.spawnUri()从指定的 URI(如本地路径或 HTTP 链接)读取脚本(源代码),并在一个全新的环境中运行它。

Isolate.run()

如下示例中的heavyTask模拟了一个CPU密集型任务,通过 Isolate.run() 启用一个新的 Isolate 对其进行计算。

// ex751.dart
import 'dart:isolate';

Future<void> main() async {
  final result = await Isolate.run(() => heavyTask(100_000_000));
  print('result: $result'); // Output:  99999999
}

int heavyTask(int count) {
  var sum = 0;
  for (int i = 0; i < count; i++) {
    sum += i % 3;
  }
  return sum;
}

Isolate.spawn()

Isolate 之间通过发送消息通信, ReceivePort 和 SendPort 是其通信端口。

  • ReceivePort(收信箱):它是私有的,只有创建它的 Isolate 才能从中读取消息。本质上它是一个 Stream。当消息掉进箱子时,Event Loop 齿轮转动,触发监听(listen)回调。
  • SendPort(投递地址): 它是公开的,可以被复制、传递给其他 Isolate。
graph LR
    subgraph Isolate_A [main Isolate]
        direction TB
        RP[ReceivePort]
        Handler([处理逻辑])
        RP -->|监听| Handler
    end

    subgraph Isolate_B [worker Isolate]
        direction TB
        SP[SendPort]
        Task([计算任务])
        Task -->|结果| SP
    end

    SP -.->|跨越物理边界| RP

    style Isolate_A fill:#f5f5f5,stroke:#333
    style Isolate_B fill:#f5f5f5,stroke:#333
    style RP fill:#FFF9C4,stroke:#FBC02D
    style SP fill:#E1F5FE,stroke:#0288D1

单向通信(“生产-消费”)

在单向通信(“生产-消费”)模式中,Main Isolate 充当“听众” (即数据的消费者),Worker Isolate 充当数据的“生产者”。

sequenceDiagram
	autonumber
    participant M as Main Isolate
    participant W as Worker Isolate
    
    M->>M: 创建 ReceivePort (RP)
    M->>W: Isolate.spawn(entryPoint, RP.sendPort)
    W->>W: 初始化自己的 Event Loop
    W->>M: 通过传入的 SendPort 发回数据
    M->>M: RP.listen 接收并处理结果
// ex752.dart
import 'dart:isolate';
import 'dart:async';

void main() async {
  final watch = Stopwatch()..start();
  var count = 0;
  void myprint(o) =>
      print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

  myprint('main: ⚙️ 启动任务...');

  final receivePort = ReceivePort(); // 1
  await Isolate.spawn(entryPoint, receivePort.sendPort); // 2

  // 3
  receivePort.listen((message) {
    // 4
    if (message is int) {
      myprint('main: progress $message%');
    } // 4a
    else if (message is String) {
      myprint('main: $message');
      receivePort.close(); // 5
      myprint('main: 🏁 done');
    }
  });

  myprint('main: 🚀 我继续做其它事');

  /* An example output:
out0 0ms: main: ⚙️ 启动任务...
out1 52ms: main: 🚀 我继续做其他事
out2 1039ms: main: progress 20%
out3 2040ms: main: progress 40%
out4 3045ms: main: progress 60%
out5 4051ms: main: progress 80%
out6 5054ms: main: progress 100%
out7 5054ms: main: DONE
out8 5061ms: main: 🏁 done  
   */
}

/// worker 逻辑: 执行任务并不断向 main “汇报”
void entryPoint(SendPort sendPort) async {
  // 模拟耗时任务,每秒发送一次进度
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    sendPort.send(20 * i); // 发送进度
  }
  // 发送最终计算结果
  sendPort.send('DONE');
}
  1. 首先创建一个ReceivePort;
  2. 启动worker Isolate;Isolate.spawn()的第一个入参,是worker的入口函数(entryPoint,代表了worker的待执行任务),第二个入参是传递给 entryPoint 的初始消息(此处是一个SendPort);
  3. receivePort本质上是一个Stream,receivePort.listen()监听worker发来的消息;
  4. 处理worker发来的消息;
  5. receivePort.close() 释放work Isolate相关资源,最终work Isolate终止。

最佳实践Isolate.spawn() 必须指向一个顶级函数或静态方法(如此例中的 entryPoint()),这是为了满足跨线程(Isolate)序列化的要求。

双向通信

我们在理解了单向通信模式之后,就不难理解双向通信模式。main isolate要想发消息给work isolate,必须通过worker的SendPort,因此我们要设法将worker的SendPort传递给main isolate。在entrypoint 函数中,worker调用mainRP.send(...)即可实现这一点(mainRP即main isolate的ReceivePort)。

sequenceDiagram
    autonumber
    participant M as Main Isolate
    participant W as Worker Isolate
    
    Note over M: 准备阶段
    M->>M: 创建 ReceivePort (mainRP)
    M->>W: Isolate.spawn(entryPoint, mainRP.sendPort)
    
    Note over W: Worker 启动
    W->>W: 初始化自己的 Event Loop
    W->>W: 创建自己的 ReceivePort (workerRP)
    W->>M: 发回 workerRP.sendPort (握手)
    
    Note over M: 建立双向连接
    M->>M: 收到并保存 Worker 的 SendPort
    
    rect rgb(232, 245, 233)
    Note over M, W: 交互阶段
    M->>W: 发送任务数据 (Main 发送消息)
    W->>W: 执行密集计算/任务
    W->>M: 通过 mainRP 发回处理结果
    end
    
    M->>M: 后续处理

如下示例由示例ex752改写而成,它演示了两个Isolate间的双向通信,请读者自行分析。

// ex753.dart
import 'dart:isolate';
import 'dart:async';

void main() async {
  final watch = Stopwatch()..start();
  var count = 0;
  String myprintPrefix() => 'out${count++} ${watch.elapsed.inMilliseconds}ms:';
  void myprint(o) => print('${myprintPrefix()} $o');

  myprint('main: ⚙️ 启动任务...');

  final receivePort = ReceivePort(); // 1
  await Isolate.spawn(entryPoint, receivePort.sendPort); // 2

  SendPort? workerSendPort;

  // 3
  receivePort.listen((message) {
    // 4
    if (message case (int x, int y)) {
      myprint('main: f($x)=$y');
    } // 4a
    else if (message is SendPort) {
      workerSendPort = message;
      myprint('main: 🤝 握手成功');
    }
  });

  myprint('main: 🚀 我继续做其它事');

  for (var x = 1; x <= 5; x++) {
    Future.delayed(
      Duration(seconds: x),
      () => workerSendPort?.send((x, myprintPrefix())),
    );
  }

  Future.delayed(Duration(seconds: 6), () => receivePort.close());

  /* An example output:
out0 0ms: main: ⚙️ 启动任务...
out1 76ms: main: 🚀 我继续做其它事
out2 138ms: main: 🤝 握手成功
out3 1089ms: worker: x=1
out4 1195ms: main: f(1)=1
out5 2090ms: worker: x=2
out6 2196ms: main: f(2)=4
out7 3096ms: worker: x=3
out8 3203ms: main: f(3)=9
out9 4097ms: worker: x=4
out10 4201ms: main: f(4)=16
out11 5096ms: worker: x=5
out12 5202ms: main: f(5)=25
   */
}

void entryPoint(SendPort mainSendPort) async {
  final workerReceivePort = ReceivePort();
  mainSendPort.send(workerReceivePort.sendPort);

  workerReceivePort.listen((message) async {
    if (message case (int x, String prefix)) {
      print('$prefix worker: x=$x');
      // Simulate a time-consuming task
      await Future.delayed(Duration(milliseconds: 100));
      final y = x * x;
      mainSendPort.send((x, y));
    }
  });
}

Isolate.spawnUri()

Isolate.spawnUri()Isolate.spawn()类似,区别在于前者的 entryPoint 是由一个脚本文件提供。我们将 ex752 中的 entryPoint 函数稍作改变,然后移至一个文件中:

// A script file for ex754.dart
import 'dart:isolate';

void main(List<String> args, SendPort sendPort) async {
  print('worker: $args');
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    sendPort.send(20 * i); // 发送进度
  }
  // 发送最终计算结果
  sendPort.send('DONE');
}

请注意观察这里的函数及其签名:原来的entryPoint变成了main函数,第二个入参代表初始消息。然后 ex752.dart 剩余的部分也稍作变化:

// ex754.dart
import 'dart:isolate';

void main() async {
  // ... (omitted for brevity)
  
  final receivePort = ReceivePort(); // 1
  var uri = Uri.file('./ex754a.dart');
  await Isolate.spawnUri(uri, ['arg1', 'arg2'], receivePort.sendPort);

  // ... (omitted for brevity)
}

请尝试运行修改后得到的新示例(ex754)并观察程序输出。

常见约束与避坑指南

  • spawnUri() 会重新加载其依赖的所有库。如果外部脚本也依赖了 dart:math,它会在新 Isolate 中拥有一份独立的拷贝。
  • 在 Flutter 环境中,由于打包机制,spawnUri()通常不能直接指向磁盘文件
  • 安全性:虽然 Isolate 之间是隔离的,但在加载外部脚本时仍需确保来源可靠。

Isolate.spawn() VS Isolate.spawnUri()

特性Isolate.spawn()Isolate.spawnUri()
代码来源当前程序中的函数(共享现有代码)外部 URI 指向的文件(独立代码)
启动速度较快(不需要重新解析代码)较慢(需要加载、解析并运行新脚本)
参数限制可以传递大部分不可变对象参数必须是简单的类型(如字符串、列表、SendPort)
内存开销相对较小较大(整个脚本环境需要重新初始化)
典型用途处理当前应用的密集计算动态插件系统、运行不受信任的外部脚本

Flutter 特色:compute()

Flutter 的 compute() 函数在用法上与 Isolate.run() 有些类似。下面是示例 ex751的Flutter/compute 版本。

// ex755.dart
import 'package:flutter/foundation.dart'; // compute

Future<void> main() async {
  final result = await compute(heavyTask, 100_000_000);
  debugPrint('result: $result'); // Output:  99999999
}

int heavyTask(int count) {
  var sum = 0;
  for (int i = 0; i < count; i++) {
    sum += i % 3;
  }
  return sum;
}

compute() 函数第一个入参数是回调函数(callback),第二入参将传递给callback(作为callback的入参)。为保持接口简洁,这里的callback只能有一个入参,如果你要传递多个参数,可以将这些参数打包成Record或Map。

跨 Isolate 传递数据

在 Dart 中,跨 Isolate 传递数据的机制被称为消息传递(Message Passing)。为了保证内存隔离的安全,Dart 限制了可以通过 SendPort 发送的对象类型。

以下是根据 Dart 官方文档整理的跨 Isolate 传递数据类型表:

类别具体数据类型说明
基础类型Null, bool, int, double, String最常用的基础数据,直接支持。
集合类型List, Map, Set容器内的元素也必须是此表中的可传递类型。
二进制数据TypedData (如 Uint8List 等)效率极高,支持通过内存移交(Transfer)机制处理。
通信对象SendPort允许发送一个端口给另一个 Isolate,实现双向通信。
函数顶级函数静态方法必须是全局可见的,不能是捕获了上下文的闭包或实例方法。
系统对象Capability, RegExp, StackTraceDart 内部定义的特殊支持对象。
自定义对象仅限 Isolate.exit() 场景在普通 send 中不支持,但在退出 Isolate 时可移交内存所有权。

注意: 当你发送一个集合(如 List)时,Dart 实际上是在目标 Isolate 中创建了一个深拷贝。这意味着在子 Isolate 中修改该 List,不会影响主 Isolate 中的原始数据。唯一的例外是使用 Isolate.exit()

Reference

零拷贝

Dart的零拷贝与Java的零拷贝指向的是完全不同的应用场景。熟悉Java的朋友知道Java 的零拷贝主要解决内核态(Kernel Space)与用户态(User Space)之间反复的数据拷贝开销。而Dart 的零拷贝是为了省去 Isolate 之间内存绝对隔离带来的通信开销。

Dart提供两种方式的零拷贝:

  • Isolate.exit()(所有权转让)
  • TransferableTypedData(字节流零拷贝,共享内存缓冲区)

Isolate.exit()

试想你要解析一个巨大的JSON字符串(如50MB),你启用一个Worker Isolate来解析它:Main Isolate将JSON字符串的文件路径或HTTP URL发给 Worker Isolate,后者将解析结果(bigMap)传回。此类应用场景正是Dart零拷贝的用武之地。我们先来看一下零拷贝方案的时序图。

sequenceDiagram
	  autonumber
    participant M as Main Isolate
    participant FS as File System / Network
    participant W as Worker Isolate (Background)

    Note over M: 监听到大数据处理请求
    M->>+W: Spawn Isolate (传递文件路径/URL)
    Note right of M: 主线程保持流畅,不加载大文件

    rect rgb(240, 248, 255)
        Note over W: 开始后台作业
        W->>FS: 读取文件或发起 HTTP 请求
        FS-->>W: 返回数据流/字符串
        W->>W: jsonDecode() 解析数据
    end

    Note over W: 解析完成,准备回传结果(bigMap)
    W->>M: Isolate.exit(resultPort, bigMap)
    Note over W: Worker 立即销毁,释放堆内存
    
    M-->>M: 接收 Map 对象 (内存所有权转让)
    Note over M: 后续处理(如更新UI)

请注意该时序图的第5步,我们使用的是 Isolate.exit(resultPort, bigMap) 而非 resultPort.send(bigMap);虽然它们都能将数据(bigMap) 传回 Main Isolate,但底层的内存处理机制完全不同。

特性SendPort.send(message)Isolate.exit(port, message)
内存行为物理拷贝 (Copying)所有权转让 (Transfer)
时间复杂度O(n)(与数据量成正比)O(1)(瞬间完成)
Worker 状态继续运行,除非手动 kill立即终止 (Terminate)
内存峰值高(主从 Isolate 同时持有副本)极低(内存块所有权转移)

虽然 Isolate.exit() 极快,但它有一个硬性限制:它是该 Isolate 发布的“遗言”。如果你需要 Worker Isolate 持续运行(例如一个长期驻留的 WebSocket 监听器),你必须使用 send(),因为 exit() 会直接杀掉当前 Isolate。

示例

// ex761.dart
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

void main() async {
  print('main: start');
  // Simulate a large json file
  final jsonpath = '${Directory.current.path}/bin/ch07/ex761.json';
  final receivePort = ReceivePort();
  Isolate.spawn(parseBigJson, (jsonpath, receivePort.sendPort)); // 1

  receivePort.listen((bigMap) {
    // 6
    if (bigMap is Map) {
      print('main: result.length=${bigMap.length}');
      if (bigMap.length < 1000) {
        print('main: result=$bigMap');
      }
      receivePort.close();
    }
  });
}

void parseBigJson((String, SendPort) message) async {
  if (message case (String jsonpath, SendPort resultPort)) {
    print('worker: paring...');
    final content = await File(jsonpath).readAsString(); // 2-3
    final bigMap = jsonDecode(content); // 4
    print('worker: parse done');
    Isolate.exit(resultPort, bigMap); // 5
  }
}

事实上Isolate.run() 内部就是通过Isolate.exit() 将结果回传给创建它的Isolate的,所以我们通常是直接调用 Isolate.run()来处理此类问题,代码更为健壮与简洁。

// ex761a.dart
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

void main() async {
  print('main: start');
  // Simulate a large json file
  final jsonpath = '${Directory.current.path}/bin/ch07/ex761.json'; // 1

  final bigMap = await Isolate.run(() async {
    print('worker: paring...');
    final file = File(jsonpath);
    final content = await file.readAsString();
    final result = jsonDecode(content);
    print('worker: parse done');
    // The result is sent using Isolate.exit, which means it's sent to main
    // isolate without copying.
    return result;
  });

  print('main: result.length=${bigMap.length}');
  if (bigMap.length < 1000) {
    print('main: result=$bigMap');
  }
}

TransferableTypedData(字节流零拷贝)

如果你需要在两个都保持存活的 Isolate 之间传递大数据(例如 Uint8List),Isolate.exit() 就不适用了。这时可以使用 TransferableTypedData,它允许你在不拷贝字节数组的情况下,将内存访问权跨 Isolate 传递。

// ex762.dart
import 'dart:isolate';
import 'dart:typed_data';

void main() async {
  final mainReceivePort = ReceivePort();
  await Isolate.spawn(worker, mainReceivePort.sendPort); // 1

  // 准备数据并包装
  final Uint8List massiveData = Uint8List(100 * 1024 * 1024); // 100MB
  final transferable = TransferableTypedData.fromList([massiveData]); // 3

  mainReceivePort.listen((message) {
    if (message is SendPort) {
      var workerSendPort = message;
      print('Main 发送 100MB 数据...');
      workerSendPort.send(transferable); // 4
      // 发送后 massiveData 在 Main Isolate 中将不再可用
    } else {
      print(message); // 8
      mainReceivePort.close(); // 8a
    }
  });
}

void worker(SendPort mainSendPort) {
  final receivePort = ReceivePort();
  mainSendPort.send(receivePort.sendPort); // 2

  receivePort.listen((message) {
    final transferable = message as TransferableTypedData;
    // 获取原始字节流(零拷贝)
    final Uint8List bytes = transferable
        .materialize() // 5 materialize
        .asUint8List(); // 6 asUint8List
    mainSendPort.send('Worker 收到 ${bytes.length / 1024 ~/ 1024} MB'); // 7
  });
}
sequenceDiagram
    autonumber
    participant M as Main Isolate
    participant W as Worker Isolate
    participant RAM as Shared Memory (C Heap)

    M->>+W: Isolate.spawn(worker, mainSendPort)
    
    Note over W: 初始化 ReceivePort
    W->>M: mainSendPort.send(workerSendPort)
    
    Note over M: 准备 100MB Uint8List
    M->>RAM: TransferableTypedData.fromList([massiveData])
    Note right of RAM: 数据从 Dart Heap 移动到可跨界访问的 C Heap

    M->>W: workerSendPort.send(transferable)
    Note over M: massiveData 在此变为不可用 (Empty)

    rect rgb(240, 248, 255)
        Note over W: 收到消息 (transferable)
        W->>RAM: transferable.materialize()
        Note right of RAM: 字节流映射到 Worker 的内存地址
        W->>W: 得到 Uint8List bytes
    end

    W->>M: mainSendPort.send("Worker 收到 100 MB")
    M-->>M: 打印消息并关闭 ReceivePort

a. 握手 (Steps 2-3): 由于 Isolate.spawn() 只能让主 Isolate 拿到 Worker 的句柄,反向通信需要 Worker 先通过 mainSendPort 把自己的 workerSendPort 发送回去。这是建立双向通信的标准做法。

b. 内存转移 (Step 4 & 6): TransferableTypedData.fromList 是核心。它将数据从 Dart 的普通堆(Heap)中移出,放到一个可以被多个 Isolate 访问的底层缓冲区(通常是 C 堆)。

  • Main 丢失:一旦 send(transferable) 执行,主 Isolate 就不再拥有这块数据。
  • Worker 获得:Worker 调用 materialize() 后,这块内存被“绑定”到 Worker 空间。

c. 零拷贝的本质: 数据本身在物理内存(RAM)中并没有被复制,只是访问权限从 Main 转移到了 Worker。

Reference

  • https://dart.dev/language/isolates
  • https://api.dart.dev/dart-isolate/Isolate-class.html

Worker Pool (Isolate 池)

Dart Worker Pool (Isolate 池)类似于Java中的线程池。在 Dart 中,Spawn 一个 Isolate 比创建线程更贵(因为它要初始化独立的堆内存)。使用Worker Pool可以 避免冷启动开销控制内存配额

  • 避免冷启动开销:如果你的应用需要频繁处理小任务(如每隔 100ms 滤镜一张图片),不断地 Isolate.run 会导致 CPU 在频繁的创建/销毁中空转。

  • 控制内存配额:由于每个 Isolate 都有独立的堆内存,无节制地开启 Isolate 会迅速导致 OOM(内存溢出)。池化可以强制将并发数限制在 CPU 核心数左右。

在 Dart 的世界里,池化不只是为了复用线程,更是为了维持一组‘热就绪’的隔离空间。如果你在做音视频剪辑、大型游戏逻辑解析,Isolate 池就是你的后台工厂。

实现方案

Worer Pool 需要维持一组长期存活的后台 Isolate,并维护它们的 SendPort,以便向其分发任务。当 Worker Pool 收到一个任务(Task)时,它分发给空闲的Worker,如果此时所有的Worker都忙,就将任务放入待执行队列(TaskQueue)。Worker Pool的核心指责就是尽快撮合 TaskWorker,一旦Worker空闲,它就去TaskQueue里取出任务交给Worker去执行。

下面是该方案的 时序图类图

sequenceDiagram
    autonumber
    participant Client as main(Task Submitter)
    participant Pool as Worker Pool (Manager)
    participant W as Worker (Isolate 0..N)

    Note over Pool, W: --- 初始化 (建立长存连接) ---
    Pool->>W: 创建并保持 Isolate 存活
    W-->>Pool: 回传通信凭证 (SendPort)
    Note over Pool: 维护 Worker 状态列表

    Note over Client, W: --- 场景 A: 任务处理 (直接执行) ---
    Client->>Pool: submit(任务 1)
    Pool->>W: 寻找空闲工人并派发
    W->>W: 执行计算
    W-->>Pool: 返回结果 1
    Pool->>Client: 交付结果 1 (Future 完成)

    Note over Client, W: --- 场景 B: 负载均衡 (排队与复用) ---
    Client->>Pool: submit(任务 2)
    Client->>Pool: submit(任务 3)
    Note over Pool: (此时工人忙碌,内部暂存任务)
    W-->>Pool: (工人忙完任务 2) 返回结果 2
    Pool->>Client: 交付结果 2
    Pool->>W: 自动派发暂存的任务 3
    W-->>Pool: 返回结果 3
    Pool->>Client: 交付结果 3
classDiagram
    class TaskData {
        +Function task
        +Record data
        +TaskData(Function task, Record data)
    }

    class Worker {
        +Isolate isolate
        +SendPort sendPort
        +bool idle
        +Completer? completer
        +Worker(Isolate isolate, SendPort sendPort)
    }

    class WorkerPool {
        +int size
        -Map~int, Worker~ _workers
        -Queue~~ _taskQueue
        -StreamController _idleWorkerController
        -ReceivePort? _resultPort
        -bool _closed
        
        +WorkerPool(int size)
        +Future~T~ submit&lt;T&gt;(TaskData taskData)
        +Future~void~ close()
    }

    class GlobalFunctions {
        <<Global>>
        +void workerEntryPoint(Record message)
    }

    WorkerPool "1" *-- "0..*" Worker : 维护并调度
    WorkerPool ..> TaskData : 处理任务请求(submit)
    Worker ..> TaskData : 通过 SendPort 发送任务给 Worker Isolate
    Worker ..> GlobalFunctions : Isolate 运行入口

该方案的示例代码如下。

// ex771.dart
import 'dart:async';
import 'dart:collection';
import 'dart:isolate';

int add((int, int) args) => args.$1 + args.$2; // 1

// 2
final watch = Stopwatch()..start();
var count = 0;
void myprint(o) => print('out${count++} ${watch.elapsed.inMilliseconds}ms: $o');

void main() async {
  var pool = WorkerPool(3); // 3
  await Future.delayed(Duration(seconds: 1)); // 3a

  const n = 9; // 4
  var results = await Future.wait([
    for (var x = 1; x <= n; x++) pool.submit(TaskData(add, (x, x))),
  ]);
  myprint(results); // 5

  await pool.close(); // 6

  /* An example output:
out0 0ms: main: 🤝 pool-worker-0 握手成功
out1 11ms: main: 🤝 pool-worker-1 握手成功
out2 18ms: main: 🤝 pool-worker-2 握手成功
out3 989ms: pool-worker-1 returns 4
out4 994ms: pool-worker-0 returns 2
out5 994ms: pool-worker-2 returns 6
out6 995ms: pool-worker-1 returns 8
out7 995ms: pool-worker-0 returns 10
out8 995ms: pool-worker-2 returns 12
out9 995ms: pool-worker-1 returns 14
out10 995ms: pool-worker-0 returns 16
out11 995ms: pool-worker-2 returns 18
out12 997ms: [2, 4, 6, 8, 10, 12, 14, 16, 18]
out13 1111ms: closed
 */
}

// 7
class TaskData {
  final Function task;
  final Record data;
  TaskData(this.task, this.data);
}

class WorkerPool {
  final String name;
  final int size; // 8
  final _workers = <int, Worker>{}; // 9
  final _taskQueue = Queue<(TaskData, Completer)>(); // 10
  final _idleWorkerController = StreamController<Worker>(); // 11

  ReceivePort? _resultPort;
  var _closed = false;

  String _workerName(int id) => '$name-worker-$id';

  WorkerPool(int size) : this.named(size, 'pool');

  WorkerPool.named(this.size, this.name) {
    _init();
    _start();
  }

  Future<void> _init() async {
    final resultPort = ReceivePort();
    final isolates = <int, Isolate>{};
    // 12
    for (int workerId = 0; workerId < size; workerId++) {
      isolates[workerId] = await Isolate.spawn(workerEntryPoint, (
        workerId,
        resultPort.sendPort,
      ), debugName: _workerName(workerId));
    }
    // 13
    resultPort.listen((message) {
      // 13b
      if (message case (int workerId, bool ok, dynamic result)) {
        _handleResult(workerId, ok, result);
      } // 13a
      else if (message case (int workerId, SendPort workerPort)) {
        _workers[workerId] = Worker(isolates[workerId]!, workerPort);
        myprint('main: 🤝 ${_workerName(workerId)} 握手成功');
      }
    });

    _resultPort = resultPort;
  }

  // 14
  void _handleResult(int workerId, bool ok, dynamic result) {
    myprint('${_workerName(workerId)} returns $result');
    final worker = _workers[workerId]!;
    if (ok) {
      worker.completer?.complete(result);
    } else {
      worker.completer?.completeError(result);
    }
    worker.completer = null;
    _idleWorkerController.sink.add(worker);
  }

  void _start() {
    // 15
    _idleWorkerController.stream.listen((worker) {
      if (_taskQueue.isNotEmpty) {
        final (task, completer) = _taskQueue.removeFirst();
        _execute(worker, task, completer);
      } else {
        worker.idle = true;
      }
    });
  }

  // 16
  void _execute(Worker worker, TaskData taskData, Completer completer) {
    worker.idle = false;
    worker.completer = completer;
    worker.sendPort.send(taskData);
  }

  // 17
  Future<T> submit<T>(TaskData taskData) {
    final completer = Completer<T>();
    if (_closed) {
      completer.completeError('worker pool closed');
    } else {
      var worker = _workers.values.where((w) => w.idle).firstOrNull;
      if (worker == null) {
        _taskQueue.add((taskData, completer));
      } else {
        _execute(worker, taskData, completer);
      }
    }
    return completer.future;
  }

  // 18
  Future close({
    bool ensureExecutingTaskCompleted = true,
    bool ensureTaskQueueEmpty = true,
  }) async {
    if (_closed) return;
    _closed = true;

    // Waiting to complete
    while (true) {
      final buzy =
          ensureExecutingTaskCompleted &&
          _workers.values.any((w) => !w.idle);
      final awaitTaskQueue = ensureTaskQueueEmpty && _taskQueue.isNotEmpty;
      if (_resultPort == null || buzy || awaitTaskQueue) {
        await Future.delayed(Duration(milliseconds: 100));
        continue;
      }
      break;
    }

    _resultPort!.close();
    _idleWorkerController.close();
    myprint('$name closed');
  }
}

// 13a
class Worker {
  final Isolate isolate;
  final SendPort sendPort;
  bool idle = true;
  Completer? completer;

  Worker(this.isolate, this.sendPort);
}

// 12a
void workerEntryPoint((int workerId, SendPort resultPort) message) {
  final workerReceivePort = ReceivePort();
  final workerId = message.$1;
  final resultPort = message.$2;
  resultPort.send((workerId, workerReceivePort.sendPort));

  workerReceivePort.listen((data) async {
    if (data case TaskData taskData) {
      try {
        var result = taskData.task.call(taskData.data);
        resultPort.send((workerId, true, result));
      } catch (e) {
        resultPort.send((workerId, false, e));
      }
    }
  });
}
  1. add 是一个全局函数,用来模拟一个计算任务。
  2. myprint 是一个辅助函数,用来打印一些日志,方便分析(在生产环境中用 logger 包)。
  3. main 函数启用了一个 WorkerPoolpoolsize=3表示将启用 3个Worker Isolate),随后等待1秒钟,让其有充分的时间初始化(方便演示)。
  4. main isolatepool 连续提交 n(此处n=9)个计算任务,并等待结果。
  5. 打印结果集。
  6. 关闭 pool,释放相关资源。
  7. TaskData 代表了待执行任务,此处简单地封装了一个函数task及其需要的参数data
  8. size 表示 Worker Isolate 的个数。
  9. _workers 形式上是一个Map(workerId->Worker),是Worker isolate及其状态(闲/忙)的列表。
  10. _taskQueue 待执行任务队列,其成员的数据类型是 Record (TaskData, Completer)
  11. _idleWorkerController 是空闲 worker 的一个 Stream,用来实现响应式(Reactive)任务调度(及时撮合taskworker)。
  12. 启动 Worker Isolate ,建立WorkerPoolWorker的双向通信,顶级函数 workerEntryPointWorker的执行入口。
  13. 处理 Worker Isolate 发来的消息,根据消息类型分别处理:a. (int workerId, SendPort workerPort) ,对应通信握手;b. (int workerId, bool ok, dynamic result),对应计算结果。
  14. _handleResult做了两件事:首先满足了调用方的 Future,然后立即通过 _idleWorkerController 宣告 Worker 可用。这正是 Reactive 编程核心:数据流动驱动逻辑执行。
  15. _idleWorkerController.listen() 监听空闲Worker事件,及时分发任务。
  16. 分发任务:将任务交给Worker Isolate去执行。
  17. submit()WorkerPool 提交任务。
  18. 关闭 WorkerPool,释放相关资源。

优雅关闭(释放资源)

现在让我们把视线聚焦在 WorkerPool.close() 方法。

  // 18
  Future close({
    bool ensureExecutingTaskCompleted = true,
    bool ensureTaskQueueEmpty = true,
  }) async {
    if (_closed) return;
    _closed = true;

    // Waiting to complete
    while (true) {
      final buzy =
          ensureExecutingTaskCompleted &&
          _workers.values.any((w) => !w.idle);
      final awaitTaskQueue = ensureTaskQueueEmpty && _taskQueue.isNotEmpty;
      if (_resultPort == null || buzy || awaitTaskQueue) {
        await Future.delayed(Duration(milliseconds: 100));
        continue;
      }
      break;
    }

    _resultPort!.close();
    _idleWorkerController.close();
    myprint('$name closed');
  }

目前代码主要存在如下问题:

  • while(true)死循环:默认参数情况下,如果某个 Isolate 内部的任务永远不返回,close() 也会永远卡住。考虑增加一个 timeout 参数,如果超过指定时间_taskQueue还没清空,强制执行后面的close逻辑(跳出 while 循环)。

  • Isolate 的销毁:目前只是关闭了WorkerPoolReceivePort_resultPort),Worker Isolate进程其实还在运行(虽然没有新任务了)。在 close 的最后,应该调用 worker.isolate.kill() 来释放系统线程资源。

  • Completer 的善后:如果用户选择了 ensureTaskQueueEmpty: false_taskQueue 中剩余任务的 Completer 应该手动 completeError(),否则调用方的 await 会永久挂起。

作为练习,请读者针对以上问题完善 WorkerPool.close()

Reference

  • https://dart.dev/language/isolates
  • https://api.dart.dev/dart-isolate/Isolate-class.html

Worker Pool(续1):残余请求问题

本节及 下一节上一节 Worker Pool (Isolate 池) 的深度延伸。本节我们将探讨并发编程中一个棘手的边缘场景:残余请求——即那些在系统关闭瞬间仍处于传输或执行状态的残留任务。

残余请求问题

在 WorkerPool 关闭时,残余请求通常以两种形式存在:

  • 在途请求(In-flight):任务已离开队列(_taskQueue),正处在发往 Worker 的途中;
  • 执行中请求(In-progress):Worker 已经接收并正在执行计算任务。

我们不妨将它们分别标记为 Q1Q2

Q1. ”在途请求“要求我们在关闭 _resultPort之前,先关闭所有workerReceivePort,以防止worker处理完任务后访问已关闭的资源(resultPort)。记住:永远先关闭‘输入端’,后关闭‘输出端’。

Q2. “执行中请求”要求我们考虑这样一个问题:在关闭 WorkerPool时,所有的Woker都必须处于空闲状态吗(即没有任务在执行中)? 这就是 WorkerPool.close()方法中 ensureExecutingTaskCompleted参数存在的原因。

解决方案

我们定义如下 WorkerPool 优雅关闭协议

sequenceDiagram
    autonumber
    participant M as WorkerPool (Main Isolate)
    participant W as Worker (Worker Isolate)

    Note over M, W: --- 阶段 1: 拦截 Q1 (断源) ---
    M->>M: 设置 _closed = true
    
    par 临界竞争
        M->>W: 发送 shutdown 指令
        M->>W: 在途任务 (Q1)
    end

    Note right of W: Worker 优先处理 #shutdown
    W->>W: 自行关闭 ReceivePort
    Note right of W: 后续抵达的 Q1 任务被自动忽略

    Note over M, W: --- 阶段 2: 消化 Q2 (排空) ---
    loop 轮询直到 busy == false
        W->>W: 继续处理执行中的 Q2 任务
        W-->>M: 回传 Q2 结果
    end

    Note over M, W: --- 阶段 3: 终结 (清理) ---
    M->>M: 关闭 _resultPort
    M->>W: 执行 isolate.kill()
    

Q1(在途请求)

在关闭 _resultPort 之前,先关闭所有workerReceivePort,该如何实现这一点呢?Worker要想收到 close 指令,必须由 WorkerPool 发送给它,我们可以将这个close指令称为shutdown消息。

首先定义_shutdownSignal常量,它表示 shutdown消息:

const _shutdownSignal = #shutdown;

然后修改 workerEntryPoint,让其处理 _shutdownSignal

void workerEntryPoint((int workerId, SendPort resultPort) message) {
  // ... (omitted for brevity)

  workerReceivePort.listen((data) async {
    if (data case TaskData taskData) {
      // ... (omitted for brevity)
    } else if (data == _shutdownSignal) {
      workerReceivePort.close();
      print('${Isolate.current.debugName} closed');
    }
  });
}

最后修改WorkerPoo.close()方法,给Worker发送_shutdownSignal:

Future close({
    bool ensureExecutingTaskCompleted = true,
    bool ensureTaskQueueEmpty = true,
    Duration timeout = const Duration(seconds: 5),
  }) async {
    if (_closed) return;
    _closed = true;

    // Notify workers to shutdown
    for (var w in _workers.values) {
      w.sendPort.send(_shutdownSignal);
    }

    // ... (omitted for brevity)
}

Q2(执行中请求)

WorkerPool.close()方法中 ensureExecutingTaskCompleted参数的默认值为true,表示WorkerPool要等到当前所有待执行任务完成后,才会去执行close逻辑(目前的代码已实现)。 而当ensureExecutingTaskCompleted 值为false时,表示强制结束当前所有的待执行任务,其参考实现如下。

  Future close({
    bool ensureExecutingTaskCompleted = true,
    bool ensureTaskQueueEmpty = true,
    Duration timeout = const Duration(seconds: 5),
  }) async {
    if (_closed) return;
    _closed = true;

    // Complete the futures
    if (!ensureExecutingTaskCompleted) {
      for (var w in _workers.values) {
        if (w.completer != null && !w.completer!.isCompleted) {
          w.completer!.completeError('worker pool force closed');
        }
      }
    }

    // ... (omitted for brevity)
  }  

结束语

本节讨论了Dart并发编程中的残余请求问题及解决方案(含代码示例),下一节将讨论 WorkerPool的最后一块拼图 —— 重启意外崩溃的 Isolate。

Worker Pool(续2):重启意外崩溃的 Isolate

由于 Isolate 拥有独立的内存堆,一个子 Isolate 的崩溃默认不会直接导致主 Isolate 崩溃,但会导致主 Isolate 永远等不到结果。处理崩溃需要结合使用 主动拦截(内部捕获)被动监听(外部监控)

主动拦截

在多 Isolate 环境中,异常不是自动共享的。如果子 Isolate 发生崩溃而没有被主动捕获,主 Isolate 只能感知到端口关闭(甚至没有任何提示),这会造成极难排查的“静默失败”。通过在 entryPoint 中对业务逻辑进行 try-catch,确保任何异常都能转换为一条“错误消息”发回给主 Isolate。

在WorkPool中,Worker Isolate 发回的业务消息是一个Record,其格式如下:

(int workerId, bool ok, dynamic result)

它代表了 (workerId, 成功标识, 计算结果或异常信息)

当业务逻辑出错时,示例ex771简单地将捕获到的异常,发回给 WorkerPool(Main Isolate)。

void workerEntryPoint((int workerId, SendPort resultPort) message) {
  // ... (omitted for brevity)

  workerReceivePort.listen((data) async {
    if (data case TaskData taskData) {
      try {
        var result = taskData.task.call(taskData.data);
        resultPort.send((workerId, true, result)); 
      } catch (e) {
        resultPort.send((workerId, false, e));
      }
    }
  });
}

实战中关于异常捕获的策略,我们可以考虑:

  1. 完整回传:将异常及异常堆栈(StackTrace)一并发回给WorkerPool;
  2. 二次封装(协议化的错误响应):将异常和/或异常堆栈采用某种格式二次封装后再发回WorkerPool;
  3. 日志记录(观测性的保障):结合异常和异常堆栈打印错误日志。

被动监听

有时候崩溃发生在 EventLoop 之外(例如内存溢出或初始化错误),Worker 根本没有机会执行 resultPort.send()。此时需要利用 Isolate.addErrorListener(),在创建 Isolate 时为其安装一个监听器,这样当Isolate崩溃时 WorkerPool 就可得到通知并做相应处理(如重启Isolate)。

sequenceDiagram
	autonumber
    participant M as WorkerPool
    participant W as Worker Isolate
    participant EP as ErrorPort

    W->>W: 发生致命错误 (如 OOM)
    W-->>EP: 自动发送错误信号
    EP->>M: 触发 ErrorListener 回调
    M->>M: 标记 Worker 为失效
    alt
      M->>M: 执行 Completer.completeError
    end
    M->>W: isolate.kill()
    M->>W: 重新 spawn 一个新 Isolate
    M->>M: 将新Worker加入池子

监听 Isolate

Isolate.addOnExitListener()往往与Isolate.addOnExitListener()配合使用,后者用于监听 Isolate 的终止(Terminate)。每个 Worker Isolate 都需要被监听,因此我们将重构 Worker 类,在其构造函数中启动监听,同时在 Worker 类中新增必要的字段。

Worker 类添加如下字段:

  final int id;
  final String name;
  final errorPort = ReceivePort();
  final exitPort = ReceivePort();
  final OnCrashCallback onCrash;

其中 OnCrashCallback 是处理Isolate 崩溃的回调函数:

typedef OnCrashCallback = Future Function(Worker, dynamic ex, dynamic stack);

启动监听

  Worker(this.id, this.name, this.isolate, this.sendPort, this.onCrash) {
    _addListeners();
  }

  void _addListeners() {
    isolate.addErrorListener(errorPort.sendPort);
    isolate.addOnExitListener(exitPort.sendPort);

    errorPort.listen((message) {
      final [ex, stack] = message;
      myprint('$name crash: $ex');
      onCrash(this, ex, stack);
    });

    exitPort.listen((_) {
      myprint('$name exit');
      errorPort.close();
      exitPort.close();
    });
  }

重启崩溃的Isolate

当检测到 Worker Isolate 崩溃时,WorkerPool 需要具备“自愈”能力(即onCrash 函数需要完成之事):

  • 处理遗留任务:给正在处理的 Completer 发送错误,或者将其任务移交给新的 Worker,防止主线程挂起。
  • 重新补员:立即终止旧的Isolate(强制释放相关资源),并创建一个新的 Isolate 来替代崩溃的那个。

为了将正在处理的任务移交给新的 Worker,我们需要记录 TaskData;同时为了防止无限次移交,给 TaskData 新增一个 maxComputation(默认为1),该字段代表了任务最多被执行多少次。以下是对应的代码片段:

class TaskData {
  final Function task;
  final Record data;
  final int maxComputation;

  TaskData(this.task, this.data) : maxComputation = 1;
  TaskData.maxComputations(this.task, this.data, this.maxComputation);
}

class Worker {
  final Isolate isolate;
  final SendPort sendPort;

  bool idle = true;
  bool died = false;

  Completer? completer;
  TaskData? taskData;
  int computations = 0;
  // ... (omitted for brevity)
}  

字段 died 用于标记 Worker.isolate 已终止,computations 用于记录任务执行次数。

下面我们开始实现 onCrash 逻辑。新增 WorkerPool._handleWorkerCrash 实例方法,它将被传入 Worker 构造函数的 onCrash 参数。

  Future _handleWorkerCrash(Worker worker, ex, stack) async {
    // Fast fail
    if (worker.taskData != null &&
        worker.computations >= worker.taskData!.maxComputation) {
      worker.complete(ex, ok: false);
    }
    worker.die();

    // Reborn Isolate
    if (_closed) return;
    myprint('reborn worker ${worker.name} ...');
    _isolates[worker.id] = await _spawn(worker.id, _resultPort!);
  }

Worker.complete()是一个实用方法,旨在让代码更加简洁:


  void complete(result, {required bool ok}) {
    if (completer != null && !completer!.isCompleted) {
      if (ok) {
        completer!.complete(result);
      } else {
        completer!.completeError(result);
      }
    }
    _resetTask();
  }

  void _resetTask() {
    taskData = null;
    completer = null;
    computations = 0;
  }

_isolates_spawn() 由原来WorkerPool._init()方法中的代码片段重构而来。

class WorkerPool {
  // ...
  
  final _isolates = <int, Isolate>{};

  Future<Isolate> _spawn(int workerId, ReceivePort resultPort) {
    return Isolate.spawn(workerEntryPoint, (
      workerId,
      resultPort.sendPort,
    ), debugName: _workerName(workerId));
  }

  // ...
}    

worker.die() 强制终止 isolate,并标记 isolate 已终止。

class Worker {
  // ... (omitted for brevity)

  void die() {
    idle = false; // To be reborned
    died = true;
    isolate.kill(priority: Isolate.immediate);
    errorPort.close();
    exitPort.close();
  }

  // ... (omitted for brevity)
}

可能有读者要问:既然 isolate 已经崩溃了,还有必要调用 isolate.kill() 吗?答案是很有必要。这是一种“防御性编程”,虽然在逻辑层面上 Isolate 已经“崩溃”了(触发了 errorListener),但崩溃并不总是意味着底层的内存和资源已经彻底释放(崩溃不等于消亡)。借助下图我们可以更好地理解这个问题。

sequenceDiagram
    participant P as WorkerPool
    participant W as Old Isolate
    participant N as New Isolate

    W->>P: 发生错误 (ErrorListener)
    Note over W: 虽报错但未彻底退出(僵尸态)
    
    rect rgb(255, 200, 200)
    Note right of P: 如果没有调用 kill
    P->>N: Isolate.spawn (启动新实例)
    Note over W, N: 此时系统中存在两个 Isolate 副本
    end

    Note over P: 资源压力增加,可能导致连续崩溃
    
    P->>W: isolate.kill(immediate)
    Note over W: 强制释放资源

新的 Isolate 接管旧的任务

下图表达了 WorkerPool的初始化(_init()方法)逻辑。

sequenceDiagram
    autonumber
    participant Main as Main Isolate (WorkerPool)
    participant RP as ReceivePort (resultPort)
    participant W1 as Worker Isolate 1
    participant W2 as Worker Isolate N

    Note over Main: _init() 开始
    Main->>RP: 创建 ReceivePort

    rect rgb(240, 240, 240)
    Note over Main, W2: 循环派生阶段 (Spawn)
    Main->>W1: Isolate.spawn(workerEntryPoint, resultPort.sendPort)
    Main->>W2: Isolate.spawn(workerEntryPoint, resultPort.sendPort)
    end

    Note over Main, RP: 进入 resultPort.listen 监听

    rect rgb(230, 255, 230)
    Note over W1, Main: 阶段一:握手协议 (Handshake)
    W1->>RP: 发送 (workerId, workerSendPort)
    RP-->>Main: 触发 listen 回调
    Main->>Main: 创建 Worker 实例
    Main->>Main: worker.takeOverTask (尝试接管旧任务)
    alt 无旧任务 (taskData == null)
        Main->>Main: 加入空闲控制器 (_idleWorkerController)
    else 有旧任务 (需重算)
        Main->>W1: _execute (重新发送任务)
    end
    Note over Main: 🤝 握手成功
    end

    rect rgb(230, 240, 255)
    Note over W1, Main: 阶段二:结果返回 (Result)
    W1->>RP: 发送 (workerId, ok, result)
    RP-->>Main: 触发 listen 回调
    Main->>Main: _handleResult(workerId, ok, result)
    end

resultPort 不仅仅用于接收计算结果,它还承担了 “控制流” 的职责。在 Worker Isolate 刚启动时,它通过这个端口回传自己的 SendPort。这种“自报家门”的机制是建立双向通讯的基础。 在握手成功的一瞬间,代码立即执行了 takeOverTask()。这在重启场景下至关重要:新 Worker 在逻辑上“透明”地替换了崩溃的旧 Worker,并立即检查是否需要继续执行未完成的任务。只有在确认没有任务需要接管时,才会将 Worker 放入 _idleWorkerController,避免了 Worker 在接管任务的同时又被分配了新任务。

具体实现如下

  Future<void> _init() async {
    final resultPort = ReceivePort();

    for (int workerId = 0; workerId < size; workerId++) {
      _isolates[workerId] = await _spawn(workerId, resultPort);
    }

    resultPort.listen((message) {
      if (message case (int workerId, bool ok, dynamic result)) {
        _handleResult(workerId, ok, result);
      } else if (message case (int workerId, SendPort workerPort)) {
        final workerName = _workerName(workerId);
        final worker = Worker(
          workerId,
          workerName,
          _isolates[workerId]!,
          workerPort,
          _handleWorkerCrash,
        );
        worker.takeOverTask(_workers[workerId]);
        _workers[workerId] = worker;

        if (worker.taskData == null) {
          _idleWorkerController.sink.add(worker);
        } else {
          // Recompute
          _execute(worker, worker.taskData!, worker.completer!);
        }
        myprint('main: 🤝 $workerName 握手成功');
      }
    });

    _resultPort = resultPort;
  }

新的_execute()方法,记录了 TaskDatacomputations(任务执行次数),以支持 任务接管takeOverTask) 和 快速失败(任务的执行次数已达上限):

 void _execute(Worker worker, TaskData taskData, Completer completer) {
    worker.idle = false;
    worker.completer = completer;
    worker.taskData = taskData;
    worker.computations++;
    worker.sendPort.send(taskData);
  }

崩溃发生时,旧 Worker 的 taskDatacompleter 被平滑转移给新 Worker,保证了业务层对底层崩溃几乎“无感知”。

 void takeOverTask(Worker? worker) {
    if (worker == null) return;
    taskData = worker.taskData;
    completer = worker.completer;
    computations = worker.computations;
  }

使用指数退避(Exponential Backoff)算法

WorkerPool_handleWorkerCrashl方法的最后一行:

    _isolates[worker.id] = await _spawn(worker.id, _resultPort!);

如果 _spawn 抛出异常(如内存不足引起IsolateSpawnException),就会导致 Isolate 重启失败。因此我们需要为这行代码进行 try-catch,当异常发生时,尝试重试。这里最好采用指数退避算法来确定重试的延迟时间(delay)。

$$delay = base \times 2^{n}$$

其中

  • base: 初始等待时间(例如 200ms)
  • n: 连续失败的次数(第 n 次重试)

为了防止延迟时间无限增长,通常会设置一个上限(max_delay):

$$delay = \min(base \times 2^{n}, \text{max_delay})$$

本文采用带随机抖动(jitter)的指数退避算法:

$$delay = \min(base \cdot 2^{n}, \text{max_delay}) + \text{jitter}$$

具体的实现如下:

  final _retryCounts = <int, int>{}; // worker reborn counts
  final Duration _baseDelay = Duration(milliseconds: 200); // reborn base delay
  final Duration _maxDelay = Duration(seconds: 60); // reborn max delay

  ///  Reborn Isolate (Exponential Backoff)
  void _reborn(Worker worker) {
    if (_closed) return;

    final id = worker.id;
    final currentRetry = _retryCounts[id] ?? 0;
    _retryCounts[id] = currentRetry + 1;

    var delay = _baseDelay * pow(2, currentRetry);
    if (delay > _maxDelay) delay = _maxDelay;
    delay += Duration(milliseconds: Random().nextInt(50)); // +jitter

    myprint(
      'reborn worker ${worker.name} ($currentRetry)'
      ' in ${delay.inMilliseconds}ms ...',
    );

    Future.delayed(delay, () async {
      if (_closed) return;
      try {
        _isolates[id] = await _spawn(worker.id, _resultPort!);
      } catch (e) {
        myprint('failed to reborn worker ${worker.name} ($currentRetry): $e');
        if (!_closed && delay < _maxDelay - Duration(milliseconds: 100)) {
          _reborn(worker);
        }
      }
    });
  }

当Worker Isolate 握手成功时,需要重置 _retryCount :

        _retryCounts[workerId] = 0; // reset reborn count

要点解析:

a. 避免群效应:代码中的随机抖动 jitter = Random().nextInt(50) ms ,看似这微小,但却是多Isolate或分布式系统中的“防洪坝”——它能有效避免多个 Isolate 在同一时刻重启而产生的 CPU 峰值(即惊群效应),因为这些重启请求将被均匀地摊分在时间轴上。

b. 递归重启: 在 catch 块中通过判断 delay < _maxDelay 再次调用_reborn,形成了一个自动重试的闭环。只要延迟没达到上限且 WorkerPool 未关闭,系统就会持续尝试恢复健康。

c. 在 Future.delayed 的开始和 _spawn 之前双重检查 if (_closed) return,确保了在 WorkerPool 关闭期间不会有“幽灵 Isolate”被创建出来。

其它细节

WorkerPool.close()在判断是否有在途任务时,需要同时考虑 Worker.idleWorker.died 两个字段。 代码片段如下

      final buzy =
          ensureExecutingTaskCompleted &&
          _workers.values.any((w) => !w.idle && !w.died);

DEMO

最后我们修改 workerEntryPoint 引入随机性的失败,然后修改main函数后运行一下我们的程序。

void workerEntryPoint((int workerId, SendPort resultPort) message) {
  // ... (omitted for brevity)

  workerReceivePort.listen((data) async {
    if (data case TaskData taskData) {
      // Simulate an uncaught exception
      if (Random().nextBool()) {
        throw 'CRASH';
      }
      // ... (omitted for brevity)
    } else if (data == _shutdownSignal) {
      workerReceivePort.close();
      print('${Isolate.current.debugName} closed');
    }
  });
}

void main() async {
  var pool = WorkerPool(3);
  await Future.delayed(Duration(seconds: 1));

  const n = 9, maxCount = 3;
  var results = await Future.wait([
    for (var x = 1; x <= n; x++)
      pool
          .submit(TaskData.maxComputations(add, (x, x), maxCount))
          .onError((e, s) => -1),
  ]);
  myprint(results);

  await pool.close();
}

/* An example output:
out0 0ms: main: 🤝 pool-worker-0 握手成功
out1 13ms: main: 🤝 pool-worker-1 握手成功
out2 18ms: main: 🤝 pool-worker-2 握手成功
out3 984ms: pool-worker-2 returns 6. computations=1
out4 989ms: pool-worker-1 crash: CRASH
out5 1007ms: reborn worker pool-worker-1 (0) in 200ms ...
out6 1008ms: pool-worker-0 crash: CRASH
out7 1008ms: reborn worker pool-worker-0 (0) in 210ms ...
out8 1009ms: pool-worker-2 crash: CRASH
out9 1009ms: reborn worker pool-worker-2 (0) in 247ms ...
out10 1237ms: main: 🤝 pool-worker-1 握手成功
out11 1237ms: pool-worker-1 crash: CRASH
out12 1237ms: reborn worker pool-worker-1 (0) in 201ms ...
out13 1241ms: main: 🤝 pool-worker-0 握手成功
out14 1242ms: pool-worker-0 crash: CRASH
out15 1242ms: reborn worker pool-worker-0 (0) in 243ms ...
out16 1293ms: main: 🤝 pool-worker-2 握手成功
out17 1294ms: pool-worker-2 returns 8. computations=2
out18 1295ms: pool-worker-2 crash: CRASH
out19 1295ms: reborn worker pool-worker-2 (0) in 244ms ...
out20 1555ms: main: 🤝 pool-worker-1 握手成功
out21 1556ms: pool-worker-1 returns 4. computations=3
out22 1556ms: pool-worker-1 crash: CRASH
out23 1556ms: reborn worker pool-worker-1 (0) in 219ms ...
out24 1571ms: main: 🤝 pool-worker-0 握手成功
out25 1571ms: pool-worker-0 returns 2. computations=3
out26 1572ms: pool-worker-0 crash: CRASH
out27 1572ms: reborn worker pool-worker-0 (0) in 200ms ...
out28 1594ms: main: 🤝 pool-worker-2 握手成功
out29 1595ms: pool-worker-2 crash: CRASH
out30 1595ms: reborn worker pool-worker-2 (0) in 206ms ...
out31 1809ms: main: 🤝 pool-worker-0 握手成功
out32 1810ms: pool-worker-0 crash: CRASH
out33 1810ms: reborn worker pool-worker-0 (0) in 228ms ...
out34 1823ms: main: 🤝 pool-worker-1 握手成功
out35 1823ms: pool-worker-1 returns 12. computations=2
out36 1824ms: pool-worker-1 crash: CRASH
out37 1824ms: reborn worker pool-worker-1 (0) in 241ms ...
out38 1838ms: main: 🤝 pool-worker-2 握手成功
out39 1838ms: pool-worker-2 crash: CRASH
out40 1839ms: reborn worker pool-worker-2 (0) in 226ms ...
out41 2063ms: main: 🤝 pool-worker-0 握手成功
out42 2063ms: pool-worker-0 returns 14. computations=3
out43 2063ms: pool-worker-0 returns 18. computations=1
out44 2091ms: main: 🤝 pool-worker-2 握手成功
out45 2097ms: main: 🤝 pool-worker-1 握手成功
out46 2097ms: pool-worker-1 crash: CRASH
out47 2098ms: reborn worker pool-worker-1 (0) in 211ms ...
out48 2336ms: main: 🤝 pool-worker-1 握手成功
out49 2336ms: pool-worker-1 returns 16. computations=3
out50 2339ms: [2, 4, 6, 8, -1, 12, 14, 16, 18]
pool-worker-2 closed
pool-worker-1 closed
pool-worker-0 closed
out51 2363ms: pool-worker-0 exit
out52 2365ms: pool-worker-2 exit
out53 2368ms: pool-worker-1 exit
out54 2451ms: pool closed

结束语

软件工程中有一句名言:“编写并发程序很难,而编写能够正确处理故障的并发程序更难。”

我们所做的每一点改进——处理残余请求的_shutdownSignal、那 50ms 的随机抖动、那看似多余的isolate.kill()、那个接管任务的握手协议——都是为了让我们的系统在面对真实的生产压力、网络波动与硬件极限时,不仅仅是“活着”,而是优雅且稳健地运行。

希望通过对 WorkerPool 的学习,能助您建立起关于防御性并发编程的思维模型。

Reference

第一个自动化测试用例

编写代码只是开发的一半,另一半则是确保这些代码在复杂的生产环境中能够持续稳定地运行。本节先简要介绍一下自动化测试,然后聚焦到单元测试,开始我们的Dart测试之旅。

自动化测试

在前面的章节中,我们编写了很多示例。让我们思考这样一问题:我如何确保代码在重构或升级后依然能正常工作?答案就是自动化测试。它不仅是质量的底线,更是开发者重构代码时的“安全网”。

自动化测试包含单元测试、组件测试、集成测试。在现代软件工程中,我们将这三者构成的体系称为测试金字塔(Test Pyramid)

  • 单元测试 (Unit Testing):测试最小的功能单元(如函数或类)。它是金字塔的基石,运行最快且成本最低。
  • 组件测试 (Component Testing):验证多个单元或 UI 组件之间的协作。
  • 集成测试 (Integration Testing):从用户视角出发,验证整个应用在真实环境下的端到端流程。
graph TD
    Top@{ shape: tri, label: "<b>集成测试</b> <br/> 链路验证" }
    Middle[/.....<b>组件测试</b>..... <br/> 模块协作\]
    Bottom[/...........<b>单元测试</b>........... <br/> 最小逻辑单元 \]

    Top --- Middle --- Bottom

    style Top fill:#f8d7da,stroke:#dc3545
    style Middle fill:#fff3cd,stroke:#ffc107
    style Bottom fill:#d4edda,stroke:#28a745

第一个单元测试用例

现代化编程语言的竞争,早已从语法层面延伸到了 工具链(Toolchain) 的完备性。而在工具链中,自动化测试框架无疑是保障工程质量的核心基石。Go 语言秉持“开箱即用”的理念,原生内置了极其强大的测试框架,不仅支持基础的单元测试,还原生支持性能基准测试(Benchmark)以及极具特色的示例测试(Example Tests)。Java拥有极为成熟的开源解决方案,从老牌的 JUnit、TestNG,到支持行为驱动开发(BDD)的 Cucumber,为企业级开发提供了极高的灵活性。Dart官方提供的 test 包(以及 Flutter 场景下的 flutter_test)深度集成在 Dart SDK 中,提供了从基础断言到异步流验证的完整解决方案。

下面我们就来编写第一个Dart单元测试用例。

环境准备

首先是用如下命令添加 test 包:

dart pub add dev:test

该命令执行完后,在 pubspec.yaml 文件中将会有如下开发依赖:

dev_dependencies:
  test: ^1.28.0

注:dev_dependencies下的包仅在开发阶段使用。

当然我们也可以先编辑 pubspec.yaml ,然后 使用 dart pub get 安装 test 包。

接下来我们将用 测试驱动开发(TDD,Test-Driven Development) 实现一个简单的分数(Fraction)类。即将创建的文件包括:

lib
├── fraction.dart
test
└── fraction_test.dart

Fraction 骨架实现

我们使用 int 作为分子、分母的数据类型,来编写 Fraction (分数),要求它自动进行约分。目前不实现任何具体的逻辑,只要求代码能通过编译。

Fraction 骨架代码

// fraction.dart
class Fraction {
  final int num;
  final int den;

  // 构造函数:留空(暂不处理约分和零分母)
  Fraction(this.num, this.den);

  // 运算符重载:全部返回一个占位结果
  Fraction operator +(Fraction other) => Fraction(0, 1);
  Fraction operator -(Fraction other) => Fraction(0, 1);
  Fraction operator *(Fraction other) => Fraction(0, 1);
  Fraction operator /(Fraction other) => Fraction(0, 1);

  @override
  bool operator ==(Object other) => false;

  @override
  int get hashCode => 0;

  @override
  String toString() => "";
}

编写单元测试(测试先行)

// fraction_test.dart
import 'package:hellodart/fraction.dart';
import 'package:test/test.dart';

void main() {
  group('Fraction 构造与约分测试', () {
    test('基本约分:2/4 应该自动变为 1/2', () {
      final f = Fraction(2, 4);
      expect(f.num, 1);
      expect(f.den, 2);
    });

    test('负号规范化:1/-3 应该变为 -1/3', () {
      final f = Fraction(1, -3);
      expect(f.num, -1);
      expect(f.den, 3);
    });

    test('分母为零应抛出异常', () {
      expect(() => Fraction(1, 0), throwsArgumentError);
    });
  });

  group('算术运算测试', () {
    final f12 = Fraction(1, 2);
    final f14 = Fraction(1, 4);

    test('加法验证:1/2 + 1/4 = 3/4', () {
      expect(f12 + f14, Fraction(3, 4));
    });

    test('减法验证:1/2 - 1/4 = 1/4', () {
      expect(f12 - f14, Fraction(1, 4));
    });

    test('乘法验证:1/2 * 1/4 = 1/8', () {
      expect(f12 * f14, Fraction(1, 8));
    });

    test('除法验证:(1/2) / (1/4) = 2/1', () {
      expect(f12 / f14, Fraction(2, 1));
    });
  });
}

group函数用于逻辑分组,expect函数用于断言。使用命令 dart test 或 在IDE(如VSCode)中运行该测试(快捷键通常是 F5),发现全红(全部报错):

Expected: <1>
  Actual: <2>

package:matcher              expect
test/fraction_test.dart 9:7  main.<fn>.<fn>
Expected: <-1>
  Actual: <1>

package:matcher               expect
test/fraction_test.dart 15:7  main.<fn>.<fn>
Expected: throws <Instance of 'ArgumentError'>
  Actual: <Closure: () => Fraction>
   Which: returned Fraction:<>

package:matcher               expect
test/fraction_test.dart 20:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 29:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 33:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 37:7  main.<fn>.<fn>
Expected: Fraction:<>
  Actual: Fraction:<>

package:matcher               expect
test/fraction_test.dart 41:7  main.<fn>.<fn>

NOTE: 使用 dart test -h 查看该命令的详细用法。

实现 Fraction (让测试变绿)

根据测试的要求去实现 Fraction 的各个方法:

// fraction.dart
class Fraction {
  late final int num;
  late final int den;

  Fraction(int n, int d) {
    if (d == 0) throw ArgumentError('分母不能为零');

    // 自动约分逻辑
    int common = _gcd(n.abs(), d.abs());
    int sign = (n * d) < 0 ? -1 : 1;

    num = (n.abs() ~/ common) * sign;
    den = d.abs() ~/ common;
  }

  /// 最大公约数
  int _gcd(int a, int b) => b == 0 ? a : _gcd(b, a % b);

  Fraction operator +(Fraction other) =>
      Fraction(num * other.den + other.num * den, den * other.den);

  Fraction operator -(Fraction other) =>
      Fraction(num * other.den - other.num * den, den * other.den);

  Fraction operator *(Fraction other) =>
      Fraction(num * other.num, den * other.den);

  Fraction operator /(Fraction other) =>
      Fraction(num * other.den, den * other.num);

  @override
  bool operator ==(Object other) =>
      other is Fraction && num == other.num && den == other.den;

  @override
  int get hashCode => Object.hash(num, den);

  @override
  String toString() => '$num/$den';
}

再次运行 fraction_test.dart,已全部通过测试(全绿)。

$ dart test
00:00 +0: test/fraction_test.dart: Fraction 构造与约分测试 基本约分:2/4 应该自动变为 1/2                         
00:00 +3: test/fraction_test.dart: Fraction 构造与约分测试 分母为零应抛出异常                                     
00:00 +4: test/fraction_test.dart: Fraction 构造与约分测试 分母为零应抛出异常                                     
00:00 +4: test/fraction_test.dart: 算术运算测试 加法验证:1/2 + 1/4 = 3/4                                         
00:00 +5: test/fraction_test.dart: 算术运算测试 加法验证:1/2 + 1/4 = 3/4                                         
00:00 +5: test/fraction_test.dart: 算术运算测试 减法验证:1/2 - 1/4 = 1/4                                         
00:00 +6: test/fraction_test.dart: 算术运算测试 减法验证:1/2 - 1/4 = 1/4                                         
00:00 +6: test/fraction_test.dart: 算术运算测试 乘法验证:1/2 * 1/4 = 1/8                                         
00:00 +7: test/fraction_test.dart: 算术运算测试 乘法验证:1/2 * 1/4 = 1/8                                         
00:00 +7: test/fraction_test.dart: 算术运算测试 除法验证:(1/2) / (1/4) = 2/1                                     
00:00 +8: test/fraction_test.dart: 算术运算测试 除法验证:(1/2) / (1/4) = 2/1                                     
00:00 +8: All tests passed!  

结束语

TDD的基本流程:红(测试失败) -> 绿(代码实现) -> 重构。TDD的好处在于:

  • 需求明确化:在写 expect(f.num, 1) 时,我们才真正确定了构造函数必须具备约分功能。
  • 接口契约:我们先写了 f12 + f14,这强迫我们决定 加法操作符参数类型返回类型
  • 重构信心:假如将来我们把 int 改成 BigInt 以支持天文数字计算时,只要运行这一套现成的单元测试,就能立即知道 重构是否破坏了原有逻辑

Dart 的 test 包非常强大,本文仅演示了基本的分组(group)和 断言(expect)功能,接下来将逐步介绍更多高级功能,包括动态匹配器、异步流验证以及强大的 Mock 机制。

Reference

断言与匹配器 (Matchers)

测试生命周期与分组

异步代码测试

依赖模拟与 Mock 实践

质量度量与工程化实践

为什么需要元编程?

build_runner

自定义生成器 (Generator)

理解 Macros(元编程的未来)

模版编程与代码脚手架(Scaffolding)

最佳实践与陷阱

dart命令行工具

SOLID编码准则

编写Dart命令行程序

Dart 服务端与云原生

Dart 常用 Package

Dart FFI