前言

如果你正在寻找一个跨平台的原生应用开发方案,你可能已经了解过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语句、泛型等概念将在后续章节陆续介绍,这里关于通配符有个印象就行。

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

参考资料

数据类型

通常计算机编程语言里的数据类型可以分为基础数据类型与复合数据类型两大类。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 encapsulate 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);
}

参考资料

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这些是命名(具名)函数。我们也可以创建没有名称的函数,称之为匿名函数,或lambda表达式 或 闭包。

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

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

语法格式:

(参数列表) {
  函数体
}

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

// 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)

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

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

如果一门编程语言具有上述特性,比如Dart,我们就说Dart支持闭包,可见闭包这个词也用来表示编程语言的这一特点(feature)。

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

// 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 等都是闭包。

练习

修改示例程序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)即可。

抛出异常 (trhow

Dart程序可以抛出(trhow)什么?答案是除了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 关键字的本意是延迟变量的初始化。

// 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);
}

方法(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 冲突;
  • 枚举的所有实例都必须在开头声明,并且至少得声明一个实例。

这些限制听起来都比较合理,也不难理解,不再赘述。

参考资料

元数据

继承

接口

类修饰符

本文中重点讨论如下类的修饰符及它们的组合

abstract

interface

base

每个类(class)都隐式地定义了一个接口(interface),因而可以实现(implements)一个类(即实现类中的方法)。base 类却是为继承而生,在定义它的文件之外,只能被继承,而不能被实现。

ex412.dart:

// ex412.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
 
}

ex412a.dart:

// ex412a.dart
import 'ex412.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. ex412.dart 文件中的Vehicle是一个 base 类,它有一个实例方法 moveForward(int);
  2. 同一文件中的 Car 实现了 Vehicle;
  3. 同一文件中的 Motorcycle 继承(extends)了 Vehicle;
  4. 另一文件( ex412a.dart )中的 Truck 继承了 Vehicle;
  5. ex412a.dart 中的 Train 实现 Vehicle,引发编译错误。

base 类有如下特点:

  • base类的子类/实现类必须使用basefinalsealed修饰,这是为了确保base的语义;
  • base类的子类自动继承了其可见的成员(字段/方法),因此为base类添加新的成员不会破坏子类,除非新成员与子类发生冲突;
  • 实例化base类的子类,必然导致base类的构造函数被调用。

sealed

final

修饰符的组合

参考资料

Mixin

扩展类

扩展方法

范型

hashCode 与 equals

列表 List

集合 Set

映射 Map

Transform 方法

异步编程概述

Future

Iterable

Stream

Isolate

test/group

测试异步代码

mockito

dart命令行工具

SOLID编码准则

编写Dart命令行程序

编写Dart服务端编程

Dart 常用package介绍