Dart 基础知识汇总

三味码屋 2023年05月23日 559次浏览

本文内容摘自 [Dart 开发语言概览] (https://dart.cn/guides/language/language-tour) 及其相关文档,做了适当精简、补充和调整,便于学习、理解、巩固 Dart 基础知识。

重要概念

  • 所有变量引用的都是 对象,每个对象都是一个 类 的实例。数字、函数以及 null 都是对象。除去 null 以外(如果你开启了 空安全), 所有的类都继承于 Object 类。
  • Dart 是强类型语言,但是在声明变量时指定类型是可选的,因为 Dart 可以进行类型推断。
  • 变量在未声明为可空类型时不能为 null。可以通过在类型后加上问号 (?) 将类型声明为可空。可以在表达式后添加 ! 来断言表达式不为空(为空时将抛出异常)
  • 如果要显式地声明变量为任意类型,使用 Object?、 Object 或者 特殊类型 dynamic 将检查延迟到运行时进行。
  • Dart 支持泛型。
  • Dart 支持顶级函数(例如 main 方法),同时还支持定义属于类或对象的函数(即 静态 和 实例方法)。还可以在函数中定义函数(嵌套 或 局部函数)。
  • Dart 支持顶级 变量,以及定义属于类或对象的变量(静态和实例变量)。
  • Dart 没有类似于 Java 那样的 public、protected 和 private 成员访问限定符。如果一个标识符以下划线 (_) 开头则表示该标识符在库内是私有的。
  • 标识符 可以以字母或者下划线 (_) 开头,其后可跟字符和数字的组合。
  • Dart 中 表达式 和 语句 是有区别的,表达式有值而语句没有。比如条件表达式 expression condition ? expr1 : expr2 中含有值 expr1 或 expr2。而 if-else 分支语句则没有值。

变量

创建变量并将其初始化:

var name = 'Bob';

默认值

在 Dart 中,未初始化以及可空类型的变量拥有一个默认的初始值 null,即便数字也是如此,因为在 Dart 中一切皆为对象。

int? lineCount;
assert(lineCount == null);

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

并不需要在声明变量时初始化,只需在第一次用到这个变量前初始化即可。

延迟初始化变量

Dart 2.12 新增了 late 修饰符,可以延迟初始化变量。

若 late 标记的变量在使用前没有初始化,在变量被使用时会抛出运行时异常。

如果一个 late 修饰的变量在声明时就指定了初始化方法,那么它实际的初始化过程会发生在第一次被使用的时候。这样的延迟初始化在以下场景中会带来便利:

  • Dart 认为这个变量可能在后文中没被使用,而且初始化时将产生较大的代价。
  • 你正在初始化一个实例变量,它的初始化方法需要调用 this。

final 和 const

如果不想更改一个变量,可以使用关键字 final 或者 const 修饰变量,这两个关键字可以替代 var 关键字或者加在一个具体的类型前。
没有使用 final 或 const 修饰的变量的值是可以被更改的。

final

一个 final 变量只可以被赋值一次,一个 const 变量是一个编译时常量 (const 变量同时也是 final 的)。

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

不能修改一个 final 变量的值。

const

使用关键字 const 修饰变量表示该变量为 编译时常量。
如果使用 const 修饰类中的变量,则必须加上 static 关键字,即 static const(顺序不能颠倒)。
在声明 const 变量时可以直接为其赋值,也可以使用其它的 const 变量为其赋值。
const 关键字不仅仅可以用来定义常量,还可以用来创建 常量值,该常量值可以赋予给任何变量。
可以将构造函数声明为 const 的,这种类型的构造函数创建的对象是不可改变的。

内置类型

Numbers

Dart 支持两种 Number 类型:

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

int 和 double 都是 num 的子类。
字符串和数字之间转换:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

Strings

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

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

在字符串中,请以 ${表达式} 的形式使用表达式,如果表达式是一个标识符,可以省略掉 {}。如果表达式的结果为一个对象,则 Dart 会调用该对象的 toString 方法来获取一个字符串。
可以使用 + 运算符或并列放置多个字符串来连接字符串:

var s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 ==
    'String concatenation works even over '
        'line breaks.');

var s2 = 'The + operator ' + 'works, as well.';
assert(s2 == 'The + operator works, as well.');

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

var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";

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

var s = r'In a raw string, not even \n gets special treatment.';

Booleans

Dart 使用 bool 关键字表示布尔类型,布尔类型只有两个对象 true 和 false,两者都是编译时常量。
Dart 的类型安全不允许使用类似 if (nonbooleanValue) 或者 assert (nonbooleanValue) 这样的代码检查布尔值。相反,应该总是显式地检查布尔值:

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);

// Check for null.
var unicorn = null;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

Lists

在 Dart 中数组由 List 对象表示。
Dart 中的列表字面量是由逗号分隔的一串表达式或值并以方括号 ([]) 包裹而组成的:

var list = [1, 2, 3];

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

可以在 Dart 的集合类型的最后一个项目后添加逗号。
List 的下标索引从 0 开始,第一个元素的下标为 0,最后一个元素的下标为 list.length - 1。
Dart 在 2.3 引入了 扩展操作符(...)和 空感知扩展操作符(...?),提供了一种将多个元素插入集合的简洁方法。例如,你可以使用扩展操作符(...)将一个 List 中的所有元素插入到另一个 List 中:

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

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

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

Dart 还同时引入了 集合中的 if集合中的 for 操作,在构建集合时,可以使用条件判断 (if) 和循环 (for)。
使用 集合中的 if 来创建一个 List 的示例,它可能包含 3 个或 4 个元素:

var nav = ['Home', 'Furniture', 'Plants', if (promoActive) 'Outlet'];

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

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

Sets

在 Dart 中,set 是一组特定元素的无序集合。
使用 Set 字面量来创建一个 Set 集合的方法:

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

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

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

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

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

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

从 Dart 2.3 开始,Set 可以像 List 一样支持使用扩展操作符(... 和 ...?)以及 Collection if 和 for 操作。

Maps

Map 是用来关联 keys 和 values 的对象。其中键和值都可以是任何类型的对象。每个 键 只能出现一次但是 值 可以重复出现多次。
使用 Map 字面量创建 Map :

var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

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

var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map<int, String>();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

向现有的 Map 中添加键值对:

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

从一个 Map 中获取一个值:

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

如果检索的 Key 不存在于 Map 中则会返回一个 null。
使用 .length 可以获取 Map 中键值对的数量。
Map 可以像 List 一样支持使用扩展操作符(... 和 ...?)以及集合的 if 和 for 操作。

Runes 与 grapheme clusters

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

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

import 'package:characters/characters.dart';

void main() {
  var hi = 'Hi 🇩🇰';
  print(hi);
  print('The end of the string: ${hi.substring(hi.length - 1)}');
  print('The last character: ${hi.characters.last}');
}

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

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

Symbols

Symbol 表示 Dart 中声明的操作符或者标识符。
Symbol 对于那些通过名称引用标识符的 API 很有用,因为代码压缩后,尽管标识符的名称会改变,但是它们的 Symbol 会保持不变。
可以使用在标识符前加 # 前缀来获取 Symbol:

#radix
#bar

函数

Dart 是一种真正面向对象的语言,所以即便函数也是对象,函数的类型为 Function,函数可以被赋值给变量或者作为其它函数的参数。
定义一个函数的例子:

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

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

isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

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

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

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

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

参数

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

命名参数

命名参数默认为可选参数,除非他们被特别标记为 required。
定义函数时,使用 {参数1, 参数2, …} 来指定命名参数。如果你没有提供一个默认值,也没有使用 required 标记的话,那么它一定是可空的类型,因为他们的默认值会是 null:

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

当调用函数时,你可以使用 参数名: 参数值 指定一个命名参数的值:

enableFlags(bold: true, hidden: false);

可以使用 = 来为一个命名参数指定除了 null 以外的默认值。指定的默认值必须要为编译时的常量:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold will be true; hidden will be false.
enableFlags(bold: true);

如果希望一个命名参数是强制需要使用的,调用者需要提供它的值,则可以使用 required 进行声明:

const Scrollbar({super.key, required Widget child});

当创建一个不带 child 参数的 Scrollbar 时,分析器就会报告这里出了问题。
一个标记了 required 的参数仍然是可空的类型:

const Scrollbar({super.key, required Widget? child});
可选的位置参数

使用 [] 将一系列参数包裹起来,即可将其标记为位置参数。因为它们的默认值是 null,所以如果没有提供默认值的话,它们的类型必须得是允许为空 (nullable) 的类型。

String say(String from, String msg, [String? device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

不使用可选参数调用上述函数:

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

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

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

可以使用 = 来为一个位置可选参数指定除了 null 以外的默认值。指定的默认值必须要为编译时的常量:

String say(String from, String msg, [String device = 'carrier pigeon']) {
  var result = '$from says $msg with a $device';
  return result;
}

assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');

main() 函数

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

void main() {
  print('Hello, World!');
}

使用命令行访问带参数的 main() 函数:

// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}

函数是一级对象

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

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// Pass printElement as a parameter.
list.forEach(printElement);

也可以将函数赋值给一个变量:

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

匿名函数

大多数方法都是有名字的,比如 main() 或 printElement()。
也可以创建一个没有名字的方法,称之为 匿名函数Lambda 表达式Closure 闭包
可以将匿名方法赋值给一个变量然后使用它,比如将该变量添加到集合或从中删除。
匿名方法看起来与命名方法类似,在括号中可以定义参数,参数之间用逗号分割。
下面代码定义了只有一个参数 item 且没有参数类型的匿名方法。 List 中的每个元素都会调用这个函数,打印元素位置和值的字符串:

const list = ['apples', 'bananas', 'oranges'];
list.map((item) {
  return item.toUpperCase();
}).forEach((item) {
  print('$item: ${item.length}');
});

词法作用域

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

词法闭包

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

/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // Create a function that adds 2.
  var add2 = makeAdder(2);

  // Create a function that adds 4.
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}

函数 makeAdder() 捕获了变量 addBy。无论函数在什么时候返回,它都可以使用捕获的 addBy 变量。

返回值

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

foo() {}

assert(foo() == null);

运算符

Dart 支持下表所示的操作符,它也体现了 Dart 运算符的关联性和 优先级 的从高到低的顺序。

DescriptionOperatorAssociativity
unary postfixexpr++ expr-- () [] ?[] . ?. !None
unary prefix-expr !expr ~expr ++expr --expr await exprNone
multiplicative* / % ~/Left
additive+ -xxx
shift<< >> >>>Left
bitwise AND&Left
bitwise XOR^Left
bitwise OR|Left
relational and type test>= > <= < as is is!None
equality== !=None
logical AND&&Left
logical OR||Left
if null??Left
conditionalexpr1 ? expr2 : expr3Right
cascade.. ?..Left
assignment= *= /= += -= &= ^= etc.Right

在 运算符表 中,运算符的优先级按先后排列,即第一行优先级最高,最后一行优先级最低,而同一行中,最左边的优先级最高,最右边的优先级最低。

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

算数运算符

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

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

示例:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder

assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart 还支持自增自减操作:

运算符描述
++varvar = var + 1 (表达式的值为 var + 1)
var++var = var + 1 (表达式的值为 var)
--varvar = var - 1 (表达式的值为 var - 1)
var--var = var - 1 (表达式的值为 var)

示例:

int a;
int b;

a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1

a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0

a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1

a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b); // -1 != 0

关系运算符

关系运算符及含义:

运算符描述
==相等
!=不等
>大于
<小于
>=大于等于
<=小于等于

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

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

类型判断运算符

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

运算符描述
as类型转换(也用作指定 库前缀)
is如果对象是指定类型则返回 true
is!如果对象是指定类型则返回 false

当且仅当 obj 实现了 T 的接口,obj is T 才是 true。例如 obj is Object 总为 true,因为所有类都是 Object 的子类。
仅当确定对象是该类型的时候,才可以使用 as 操作符把对象转换为特定的类型:

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

如果不确定这个对象的类型是不是 T,需要在转型前使用 is T 检查类型:

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

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

赋值运算符

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

// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;

除了 = 和 ??=,还有将算数运算符和赋值运算符组合在一起的赋值运算符:

运算符运算符运算符运算符运算符
+=-=*=/=%=
~/=>>>=<<=>>=^=
&=|==??=

复合运算符的原理:

运算符复合运算等效表达式
opa op= ba = a op b

逻辑运算符

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

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

使用逻辑表达式的示例:

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

位运算符

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

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

位运算符示例:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right
assert((value >>> 4) == 0x02); // Unsigned shift right
assert((-value >> 4) == -0x03); // Shift right
assert((-value >>> 4) > 0); // Unsigned shift right

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

条件表达式

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

  • 条件 ? 表达式 1 : 表达式 2
    如果条件为 true,执行表达式 1并返回执行结果,否则执行表达式 2 并返回执行结果。
  • 表达式 1 ?? 表达式 2
    如果表达式 1 为非 null 则返回其值,否则执行表达式 2 并返回其值。

级联运算符

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

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

如果对象可能为null:

var paint = getPaint()
  ?..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

级联运算符可以嵌套:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

返回值为 void 的方法不能使用级联运算符:

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

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

其他运算符

运算符名字描述
()使用方法代表调用一个方法
[]访问 List访问 List 中特定位置的元素
?[]判空访问 List左侧调用者不为空时,访问 List 中特定位置的元素
.访问成员成员访问符
?.条件访问成员与上述成员访问符类似,但是左边的操作对象不能为 null,例如 foo?.bar,如果 foo 为 null 则返回 null ,否则返回 bar
!空断言操作符将表达式的类型转换为其基础类型,如果转换失败会抛出运行时异常。例如 foo!.bar,如果 foo 为 null,则抛出运行时异常

流程控制语句

if 和 else

Dart 支持 if - else 语句,其中 else 是可选的。
Dart 的 if 语句中的条件必须是布尔值而不能为其它类型。

for循环

标准 for 循环:

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

在 Dart 中,for 循环中的闭包会自动捕获循环的索引值:

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}

for (final c in callbacks) {
  c();
}

上述代码执行后会输出 0 和 1,但如果在 JavaScript 中执行同样的代码则会输出两个 2。
如果不需要知道当前的遍历索引,则可以使用 for-in 方法进行遍历:

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

也可以使用 forEach() 方法进行遍历:

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

while 和 do-while

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

while (!isDone()) {
  doSomething();
}

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

do {
  printLine();
} while (!atEndOfPage());

break 和 continue

使用 break 中断循环:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

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

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

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

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

switch 和 case

Switch 语句在 Dart 中使用 == 来比较整数、字符串或编译时常量,比较的两个对象必须是同一个类型且不能是子类并且没有重写 == 操作符。
枚举类型非常适合在 Switch 语句中使用。
每一个非空的 case 子句都必须有一个 break 语句,也可以通过 continue、throw 或者 return 来结束非空 case 语句。
不匹配任何 case 语句的情况下,会执行 default 子句中的代码:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

忽略了 case 子句的 break 语句,会产生错误:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // ERROR: Missing break

  case 'CLOSED':
    executeClosed();
    break;
}

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

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // Empty case falls through.
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

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

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // Continues executing at the nowClosed label.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

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

断言

在开发过程中,可以在条件表达式为 false 时使用 assert(条件, 可选信息); — 语句来打断代码的执行:

// Make sure the variable has a non-null value.
assert(text != null);

// Make sure the value is less than 100.
assert(number < 100);

// Make sure this is an https URL.
assert(urlString.startsWith('https'));

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

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

assert 的第一个参数可以是值为布尔值的任何表达,如果表达式的值为 true,则断言成功,继续执行。如果表达式的值为 false,则断言失败,抛出一个 AssertionError 异常。
断言是否生效依赖开发工具和使用的框架:

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

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

异常

Dart 可以抛出和捕获异常。
异常表示一些未知的错误情况,如果异常没有捕获则会被抛出从而导致抛出异常的代码终止执行。
与 Java 不同的是,Dart 的所有异常都是非必检异常,方法不必声明会抛出哪些异常,并且也不必捕获任何异常。
Dart 提供了 ExceptionError 两种类型的异常以及它们一系列的子类,也可以定义自己的异常类型。
Dart 中可以将任何非 null 对象作为异常抛出而不局限于 ExceptionError 类型。

抛出异常

抛出或者引发异常的示例:

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

抛出任意对象:

throw 'Out of llamas!';

优秀的代码通常会抛出 Error 或 Exception 类型的异常。

捕获异常

捕获异常可以避免异常继续传递(重新抛出异常除外)。捕获一个异常可以对其进行处理:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

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

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

可以使用 oncatch 来捕获异常,使用 on 来指定异常类型,使用 catch 来捕获异常对象,两者可同时使用。
可以为 catch 方法指定两个参数,第一个参数为抛出的异常对象,第二个参数为栈信息 StackTrace 对象:

try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

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

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // Runtime error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}

finally

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

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

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

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

Dart 是支持基于mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null 以外的所有的类都继承自 Object 类。
基于mixin的继承意味着尽管每个类(top class Object? 除外)都只有一个超类,但一个类的代码可以在其它多个类继承中重复使用。

使用类的成员

对象的成员函数数据(即方法实例变量)组成。
使用 . 来访问对象的实例变量或方法:

var p = Point(2, 2);

// Get the value of y.
assert(p.y == 2);

// Invoke distanceTo() on p.
double distance = p.distanceTo(Point(4, 4));

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

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

使用构造函数

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

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

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

var p = const ImmutablePoint(2, 2);

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

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // They are the same instance!

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

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

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

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

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

var a = const ImmutablePoint(1, 1); // Creates a constant
var b = ImmutablePoint(1, 1); // Does NOT create a constant

assert(!identical(a, b)); // NOT the same instance!

获取对象的类型

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

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

实例变量

声明实例变量示例:

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

所有未初始化的实例变量其值均为 null
所有实例变量均会隐式地声明一个 Getter 方法。非终值的实例变量和 late final 声明但未声明初始化的实例变量还会隐式地声明一个 Setter 方法。
如果在实例中声明了没有 late 修饰的变量,它会在实例初始化时早于构造方法进行赋值。因此,没有使用 late 修饰的变量无法访问到 this

构造函数

声明一个与类名一样的函数即可声明一个构造函数。大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:

class Point {
  double x = 0;
  double y = 0;

  Point(double x, double y) {
    // See initializing formal parameters for a better way
    // to initialize instance variables.
    this.x = x;
    this.y = y;
  }
}

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

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

终值初始化

对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤。
构造中初始化的参数可以用于初始化非空或 final 修饰的变量,它们都必须被初始化或提供一个默认值:

class Point {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  Point(this.x, this.y);
}

在初始化时出现的变量默认是隐式终值,且只在初始化时可用。

默认构造函数

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

构造函数不被继承

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

命名式构造函数

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

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

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

调用父类非默认构造函数

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

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

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

class Person {
  String? firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson().
  Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
  }
}

void main() {
  var employee = Employee.fromJson({});
  print(employee);
  // Prints:
  // in Person
  // in Employee
  // Instance of 'Employee'
}

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

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

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

超类参数

为了不重复地将参数传递到超类构造的指定参数,可以直接在子类的构造中使用超类构造的某个参数。
超类参数不能和重定向的参数一起使用。超类参数的表达式和写法与终值初始化类似:

class Vector2d {
  final double x;
  final double y;

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

class Vector3d extends Vector2d {
  final double z;

  // Forward the x and y parameters to the default super constructor like:
  // Vector3d(final double x, final double y, this.z) : super(x, y);
  Vector3d(super.x, super.y, this.z);
}

如果超类构造的位置参数已被使用,那么超类构造参数就不能再继续使用被占用的位置。但是超类构造参数可以始终是命名参数:

class Vector2d {
  // ...

  Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
  // ...

  // Forward the y parameter to the named super constructor like:
  // Vector3d.yzPlane({required double y, required this.z})
  //       : super.named(x: 0, y: y);
  Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}

使用超类参数需要 Dart SDK 版本 至少为 2.17。在先前的版本中,你必须手动传递所有的超类构造参数。

初始化列表

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

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

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

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

Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}
重定向构造函数

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

class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(double x) : this(x, 0);
}
常量构造函数

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

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

常量构造函数创建的实例并不总是常量。

工厂构造函数

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

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache = <String, Logger>{};

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

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

在工厂构造函数中无法访问 this。工厂构造函数的调用方式与其他构造函数一样。

方法

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

实例方法

对象的实例方法可以访问实例变量和 this:

import 'dart:math';

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}
操作符

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

操作符操作符操作符操作符
<+|>>>
>/^[]
<=~/&[]=
>=*<<~
-%>>==

一些操作符没有出现在列表中,例如 !=,因为它们仅仅是语法糖,表达式 e1 != e2 仅仅是 !(e1 == e2) 的一个语法糖。
为了表示重写操作符,我们使用 operator 标识来进行标记。
下面是重写 +- 操作符的例子:

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  @override
  bool operator ==(Object other) =>
      other is Vector && x == other.x && y == other.y;

  @override
  int get hashCode => Object.hash(x, y);
}

void main() {
  final v = Vector(2, 3);
  final w = Vector(2, 2);

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}
Getter 和 Setter

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

class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

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

抽象方法

实例方法、Getter 方法以及 Setter 方法都可以是抽象的。
抽象方法是指定义一个接口方法而不去做具体的实现,让实现它的类去实现该方法。
抽象方法只能存在于抽象类中。
直接使用分号(;)替代方法体即可声明一个抽象方法:

abstract class Doer {
  // Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}

抽象类

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

隐式接口

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

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final String _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  String get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

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

class Point implements Comparable, Location {...}

扩展一个类

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

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}
重写类成员

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

class Television {
  // ···
  set contrast(int value) {...}
}

class SmartTelevision extends Television {
  @override
  set contrast(num value) {...}
  // ···
}

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

noSuchMethod 方法

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

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

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

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

扩展方法

扩展方法是向现有库添加功能的一种方式。
这里是一个在 String 中使用扩展方法的样例,我们取名为 parseInt(),它在 string_apis.dart 中定义:

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

枚举类型

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

所有的枚举都继承于 Enum 类。枚举类是封闭的,即不能被继承、被实现、被 mixin 混入或显式被实例化。
抽象类和 mixin 可以显式的实现或继承 Enum,但只有枚举可以实现或混入这个类,其他类无法享有同样的操作。

声明简单的枚举

使用关键字 enum 来定义简单的枚举类型和枚举值:

enum Color { red, green, blue }
声明增强的枚举类型

Dart 中的枚举也支持定义字段、方法和常量构造,常量构造只能构造出已知数量的常量实例(已定义的枚举值)。
可以使用与定义 类 类似的语句来定义增强的枚举,但是有一些限制条件:

  • 实例的字段必须是 final,包括由 mixin 混入的字段。
  • 所有的 实例化构造 必须以 const 修饰。
  • 工厂构造 只能返回已知的一个枚举实例。
  • 由于 Enum 已经自动进行了继承,所以枚举类不能再继承其他类。
  • 不能重载 index、hashCode 和比较操作符 ==。
  • 不能声明 values 字段,否则它将与枚举本身的静态 values getter 冲突。
  • 在进行枚举定义时,所有的实例都需要首先进行声明,且至少要声明一个枚举实例。
    下方是一个增强枚举的例子,包含多个枚举实例、成员变量、getter 并且实现了接口:
enum Vehicle implements Comparable<Vehicle> {
  car(tires: 4, passengers: 5, carbonPerKilometer: 400),
  bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
  bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);

  const Vehicle({
    required this.tires,
    required this.passengers,
    required this.carbonPerKilometer,
  });

  final int tires;
  final int passengers;
  final int carbonPerKilometer;

  int get carbonFootprint => (carbonPerKilometer / passengers).round();

  @override
  int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}
使用枚举

可以像访问 静态变量 一样访问枚举值:

final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
  print('Your favorite color is blue!');
}

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

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

每一个枚举值都有一个名为 name 的成员变量的 Getter 方法,该方法将会返回枚举值的名称:

print(Color.blue.name); // 'blue'

使用枚举类的 values 方法获取全部枚举值的列表:

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

可以在 Switch 语句中使用枚举,但是每一个枚举值都必须成为一个 case 子句,不然会出现警告:

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
}

使用 Mixin 为类添加功能

Mixin 是一种在多重继承中复用某个类中代码的方法模式。
使用 with 关键字并在其后跟上 Mixin 类的名字来使用 Mixin 模式:

class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

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

mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

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

class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // ...
}

mixin 关键字在 Dart 2.1 中才被引用支持。早期版本中的代码通常使用 abstract class 代替。

mixin示例:

class A {

  String? nameA;

  void doA(Object something) {
    print(something);
  }
}

class B {
  String? nameB;

  void doB(Object something) {
    print(something);
  }
}

/// C 指定 B 可以 Mixin C
mixin C on B {
  String? nameC;

  void doC(Object something) {
    print(something);
  }
}

class D extends B with C {
  String? nameD;

  void doD(Object something) {
    print(something);
  }
}

/// E 不能 mixin C
class E with C {
  String? nameE;

  void doE(Object something) {
    print(something);
  }
}

void test() {
  A a = A();
  a.doA("something");
  a.doC("something"); // a 不能调用 C.doC
  String? nameA = a.nameA;
  String? nameC = a.nameC; // a 不能调用 C.nameC

  B b = B();
  b.doB("something");
  b.doC("something"); // b 不能调用 C.doC
  String? nameB = b.nameB;
  nameC = b.nameC; // b 不能调用 C.nameC

  C c = C(); // C 不能构造实例

  D d = D();
  d.doB("something");
  d.doC("something");
  String? nameD= d.nameD;
  nameB = d.nameB;
  nameC = d.nameC;
}

类变量和方法

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

静态变量

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

class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}

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

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

静态方法

静态方法(即类方法)不能对实例进行操作,因此不能使用 this。可以在一个类上直接调用静态方法:

import 'dart:math';

class Point {
  double x, y;
  Point(this.x, this.y);

  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

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

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

泛型

<…> 符号表示泛型,通常使用一个字母来代表类型参数,比如 E、T、S、K 和 V等。

为什么使用泛型?

  • 可以更好地帮助代码生成。
  • 可以减少代码重复。
使用集合字面量

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

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

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

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

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

创建一个键为 Int 类型,值为 View 类型的 Map 对象:

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

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

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

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

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

限制参数化类型

使用泛型的时候,如果要限制泛型范围,可以使用 extends 关键字。

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

class Extender extends SomeBaseClass {...}

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

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

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

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

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

使用泛型方法

方法和参数也可以使用类型参数:

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

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

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

库和可见性

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

使用库

使用 import 来指定命名空间以便其它库可以访问。
例如导入代码库 dart:html 来使用 Dart Web 中相关 API:

import 'dart:html';

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

import 'package:test/test.dart';
指定库前缀

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

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();
导入库的一部分

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

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;
延迟加载库

延迟加载(懒加载)允许在需要时再去加载代码库。延迟加载使用场景:

  • 减少应用初始化时间。
  • A/B 测试,如测试各种算法的不同实现。
  • 加载很少会使用到的功能。

目前只有 dart compile js 支持延迟加载,Flutter 和 Dart VM 目前都不支持延迟加载。

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

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

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

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

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

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

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

实现库

异步支持

Dart 代码库中有大量返回 FutureStream 对象的函数,这些函数都是异步的,它们会在耗时操作(比如 I/O )执行完毕前直接返回而不会等待耗时操作执行完毕。
asyncawait 关键字用于实现异步编程,并且让你的代码看起来就像是同步的一样。

处理 Future

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

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

尽管 async 函数可能会执行一些耗时操作,但是并不会等待耗时操作完成,相反,会在遇到第一个 await 表达式时返回一个 Future 对象,然后等待 await 表达式执行完毕后继续执行。
使用 trycatch 以及 finally 来处理使用 await 导致的异常:

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

可以在异步函数中多次使用 await 关键字:

var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await 表达式的返回值通常是一个 Future 对象;如果不是的话也会自动将其包裹在一个 Future 对象里。 Future 对象代表一个“承诺”, await 表达式会阻塞直到需要的对象返回。

声明异步函数

异步函数是函数体由 async 关键字标记的函数。
将关键字 async 添加到函数并让其返回一个 Future 对象:

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

函数体不需要使用 Future API。如有必要,Dart 会创建 Future 对象。
如果函数没有返回有效值,需要设置其返回类型为 Future<void>

处理 Stream

Stream 中获取值,可以有两种选择:

  • 使用 async 关键字和一个异步循环(使用 await for 关键字标识)。
  • 使用 Stream API。
    使用 await for 定义异步循环看起来是这样的:
await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

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

  1. 等待直到 Stream 返回一个数据。
  2. 使用 1 中 Stream 返回的数据执行循环体。
  3. 重复 1、2 过程直到 Stream 数据返回完毕。
    使用 breakreturn 语句可以停止接收 Stream 数据,这样就跳出了循环并取消注册监听 Stream。

生成器

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

  • 同步生成器:返回一个 Iterable 对象。
  • 异步生成器:返回一个 Stream 对象。
    通过在函数上加 sync* 关键字并将返回值类型设置为 Iterable 来实现一个同步生成器函数,在函数中使用 yield 语句来传递值:
Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

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

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

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

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

可调用类

通过实现类的 call() 方法,允许使用类似函数调用的方式来使用该类的实例。
所有的类都可以定义并模拟 call() 方法,call() 方法与普通函数是一样的,支持传参和定义返回类型等:

class WannabeFunction {
  String call(String a, String b, String c) => '$a $b $c!';
}

var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');

void main() => print(out);

隔离区

为了解决多线程带来的并发问题,Dart 使用 isolate 替代线程,所有的 Dart 代码均运行在一个 isolate 中,而非线程。每一个 isolate 都有一个单独的执行线程,并且不与其他的 isolate 共享任何可变对象。

Typedefs

类型别名是引用某一类型的简便方法,因为其使用关键字 typedef,因此通常被称作 typedef。示例:

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

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

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

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

typedef Compare<T> = int Function(T a, T b);

int sort(int a, int b) => a - b;

void main() {
  assert(sort is Compare<int>); // True!
}

元数据

使用元数据可以为代码增加一些额外的信息。元数据注解以 @ 开头,其后紧跟一个编译时常量(比如 deprecated)或者调用一个常量构造函数。
Dart 中有两个注解是所有代码都可以使用的: @deprecated@Deprecated@override
使用 @deprecated 的示例:

class Television {
  /// Use [turnOn] to turn the power on instead.
  @Deprecated('Use turnOn instead')
  void activate() {
    turnOn();
  }

  /// Turns the TV's power on.
  void turnOn() {...}
  // ···
}

可以自定义元数据注解。
定义带有两个参数的 @Todo 注解::

class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}

使用 @Todo 注解:

@Todo('Dash', 'Implement this function')
void doSomething() {
  print('Do something');
}

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

注释

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

单行注释

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

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

多行注释

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

void main() {
  /*
   * This is a lot of work. Consider raising chickens.

  Llama larry = Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}

文档注释

文档注释可以是多行注释,也可以是单行注释。
文档注释以 /// 或者 /** 开始。在连续行上使用 /// 与多行文档注释具有相同的效果。
在文档注释中,除非用中括号括起来,否则分析器会忽略所有文本。使用中括号可以引用类、方法、字段、顶级变量、函数和参数。括号中的符号会在已记录的程序元素的词法域中进行解析。
引用其他类和成员的文档注释:

/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
///
/// Just like any other animal, llamas need to eat,
/// so don't forget to [feed] them some [Food].
class Llama {
  String? name;

  /// Feeds your llama [food].
  ///
  /// The typical llama eats one bale of hay per week.
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

在生成的文档中,[feed] 会成为一个链接,指向 feed 方法的文档, [Food] 会成为一个链接,指向 Food 类的 API 文档。
解析 Dart 代码并生成 HTML 文档,可以使用 Dart 的文档生成工具 dart doc

参考文档

1.Dart 开发语言概览