Dart 特殊语法

Dart 是一种由 Google 开发的面向对象的编程语言,主要用于构建跨平台应用。相对于其它开发语言,它有一些独特的语法和功能,这篇文章将这些功能记录如下,以方便查阅。

变量

Dart 语言中,可以使用 var 声明变量,而无需显示指定它们的类型,这些变量的类型由它们的初始值确定:

1
2
3
4
5
6
7
8
var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
'tags': ['saturn'],
'url': '//path/to/saturn.jpg'
};

另外,也可以显示声明变量的类型:

1
2
// name 的类型为 String
String name = 'Bob';

空安全

默认情况下,声明变量时,变量不允许为空,在使用前必须赋值,如果没有赋值,则编译器会报错,禁止访问非空变量。如果这个变量可以为空,则需要在类型声明的末尾添加 ?:

1
2
3
String? name;  // 可空类型。可以是 `null` 或字符串。

String name; // 不可空类型。不能是 `null` ,但可以是字符串

延迟变量

可以使用 late 声明一个变量为延迟变量,它主要用在声明该变量时其值还不能确定或初始化成本很高的场景,需要延迟赋值。如下所示:

1
late String description;

final 和 const

如果变量的值初始化之后就会再改变,可以使用 finalconst 来定义。其中,final 变量表示只能设置一次,一般是初始后之后将不再改变,而const 变量是编译时常量,在编译时就要确定变量的值。

1
2
3
4
5
6
// 初始化之后将不同改变
final String nickname = 'Bobby';

// 编译时常量
const bar = 1000000; // 压力单位 (dynes/cm2)
const double atm = 1.01325 * bar; // 标准大气压

运算符

赋值运算符

常规的赋值运算符为 =,如果要仅在被赋值变量为 null 时赋值,请使用 ??= 运行符。

1
2
3
4
// 将值赋给 a
a = value;
// 如果 b 为 null,则将值赋给 b;否则,b 保持不变
b ??= value;

条件表达式

Dart 有两个运算符:

  1. 如果 conditiontrue, 返回 expr1,否则返回 exp2.

    1
    condition  ?  expr1  :  expr2

  2. 如果 expr1 不为 null, 则返回其值;否则,返回 exp2.

    1
    expr1  ??  expr2

实例:

1
2
3
var visibility = isPublic ? 'public' : 'private'

tring playerName(String? name) => name ?? 'Guest';

级联表示法

级联(..., ?...) 允许对同一个对象执行一系列的操作,编写更流畅的代码。其中, ... 针对非空对象,而 ?... 针对可空对象。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... 操作
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;

// 等同于如下代码:
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

// ?... 操作
querySelector('#confirm') // 获取对象。
?..text = 'Confirm' // 使用其成员。
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();

展开运算符

展开运算符(..., ?...)将一个集合解包,加到另外一个集合中。其中 ...? 表示集合可为空。

1
2
3
4
5
6
7
// ... 操作
var list = [1, 2, 3];
var list2 = [0, ...list];

// ...? 操作
var list2 = [0, ...?list];

类型

记录

记录是一种包含多种类型的聚合类型,可以用在返回多个参数或传入多个参数的场景中,语法如下所示:

1
var record = ('first', a: 2, b: true, 'last');
在记录中可以包含多个类型的字段,如 String、int和 bool 类型。同时,也可以对字段进行命令,如 a 和 b。

记录的类型

记录的类型使用逗号分隔的括在括号中的类型列表来表示,如 (int, int), 定义如下:

1
2
3
4
(int, int) swap((int, int) record) {
var (a, b) = record;
return (b, a);
}

命名字段

在记录类型中,可以对字段进行命名,如下所示:

1
2
3
4
5
// 变量声明中的记录类型:
({int a, bool b}) record;

// 用记录表达式初始化它:
record = (a: 123, b: true);

命令位置

在记录类型中,也可以命名 位置 字段,不会影响记录的类型,它的使用与正常的记录一样。如下所示:

1
2
3
(int a, int b) recordAB = (1, 2);
(int x, int y) recordXY = (3, 4);

字段查询

记录字段可通过内置 getter 访问。记录是不可变的,因此字段没有 setter。 命名字段公开同名的 getter。位置字段公开名为 $<position> 的 getter,跳过命名字段:

1
2
3
4
5
6
var record = ('first', a: 2, b: true, 'last');

print(record.$1); // 打印 'first'
print(record.a); // 打印 2
print(record.b); // 打印 true
print(record.$2); // 打印 'last'

模式匹配

记录允许函数返回多个值,要从返回值中访问记录值,可以使 模式匹配 将值解构为局部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在记录中返回多个值:
(String name, int age) userInfo(Map<String, dynamic> json) {
return (json['name'] as String, json['age'] as int);
}

final json = <String, dynamic>{
'name': 'Dash',
'age': 10,
'color': 'blue',
};

// 使用带有位置字段的记录模式解构:
var (name, age) = userInfo(json);

/* 等同于:
var info = userInfo(json);
var name = info.$1;
var age = info.$2;
*/

也可以使用 命名字段 解构记录,使用冒号 : 语法。

1
2
3
4
({String name, int age}) userInfo(Map<String, dynamic> json)
// ···
// 使用带有命名字段的记录模式解构:
final (:name, :age) = userInfo(json);

列表

Dart 列表用方括号 ([]) 括起来的表达式来表示,使用逗号进行分隔,为了避免出错,可以在最后一个值之后添加一个逗号。

1
2
3
4
5
6
7
8
var list = [1, 2, 3];

// 可以在最后添加一个逗号
var list = [
'Car',
'Boat',
'Plane',
];

列表使用索引进行访问,其中 0 是第一个值的索引, list.length - 1 是最后一个值的索引。您可以使用 .length 属性获取列表的长度,并使用下标运算符 ([]) 访问列表的值。

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

list[1] = 1;
assert(list[1] == 1);

集合

Dart 使用集合 Set 来表示无序的集合,要创建一个空集合,请使用以类型参数为前缀的 {} ,或将 {} 分配给 Set 类型的变量。

1
2
3
4
5
6
7
// 定义一个简单的集合
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

// 定义一个空集合
var names = <String>{};
// Set<String> names = {}; // 这也行得通。
// var names = {}; // 创建一个映射,而不是一个集合。

使用 add()addAll() 方法向现有集合中添加项目:

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

使用 .length 获取集合中项目的数量:

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

assert(elements.length == 5);

映射

映射就是常见的 Map 键和值,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
var gifts = {
// 键:值
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};

var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
使用 [] 添加和访问映射键值对。

1
2
3
4
5
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // 添加键值对

// 访问
assert(gifts['first'] == 'partridge');

如果您查找映射中不存在的键,则会返回 null

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

使用 .length 获取映射中键值对的数量:

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

运算符

展开运算符

Dart 在列表、映射和集合中支持展开运算符 (...) 和空感知展开运算符 (...?)。展开运算符提供了一种简洁的方式将多个值插入集合中。 可以使用展开运算符 (...) 将列表的所有值插入另一个列表中:

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

如果展开运算符右侧的表达式可能为 null,则可以使用空感知展开运算符 (...?) 来避免异常:

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

控制流运算符

Dart 提供集合 iffor 用于列表、映射和集合变量。可以使用这些运算符使用条件语句 (if) 和重复语句 (for) 来构建集合。

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

Dart 还支持集合字面量中的 if-case

1
var nav = ['Home', 'Furniture', 'Plants', if (login case 'Manager') 'Inventory'];

使用 集合 for 将一个集合对象加入另外一个集合对象中:

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

模式

模式类似于其它语言中的 正则表达式,它主要有两个作用,分别是 匹配解构匹配主要是根据上下文和模式的形状来进行判断 模式匹配对象 是否匹配。而解构则是从 匹配对象 中取出对应的值。

模式匹配主要从下面几个方面判断 模式给定值(匹配对象)是否匹配:

  • 具有某种形状;
  • 是某个常量;
  • 等于其他内容;
  • 具有某种类型。

模式匹配等同于返回一个 bool 值,可以在 if,switchfor 语句中使用,如下所示:

1
2
3
4
5
6
7
const a = 'a';
const b = 'b';
switch (obj) {
// 如果 obj 是一个包含两个字段的列表,则列表模式 [a, b] 首先匹配 obj,然后如果其字段匹配常量子模式 'a' 和 'b'。
case [a, b]:
print('$a, $b');
}

当对象和模式匹配时,模式可以访问对象的数据并将其分配给指定的变量,这便是 解构 对象。解构 对象需要定义变量,如下所示:

1
2
3
4
5
var numList = [1, 2, 3];
// 列表模式 [a, b, c] 解构 numList 中的三个元素...
var [a, b, c] = numList;
// ...并将它们分配给新的变量。
print(a + b + c);
模式变量声明必须以 varfinal 开头,后跟一个模式。

模式有几种主要类型,下面进行分析。

空检查

定义为: subpattern?,如果值不为 null, 模式匹配成功,模式中同时可以给定一个变量,即 解构变量,如果值为空,则匹配失败。

1
2
3
4
5
String? maybeString = 'nullable with base type String';
switch (maybeString) {
case var s?:
// 's' 在此处具有非空 String 类型。
}
空检查不会抛出异常。

空断言

定义为:subpattern!, 如果对象不为空,则进行匹配和解构,如果为空,则抛出异常。

1
2
3
4
5
List<String?> row = ['user', null];
switch (row) {
case ['user', var name!]: // ...
// 'name' 在此处是非空字符串。
}

常量

形如后面的定义:123, null, 'string', math.pi, SomeClass.constant, const Thing(1, 2), const (1 + 2)  当值等于常量时,常量模式匹配成功:

1
2
3
4
switch (number) {
// 如果 1 == number,则匹配。
case 1: // ...
}

变量

定义为:var bar, String str, final int _, 变量模式将新变量绑定到匹配的值上,它们主要是获取对象上的值。

1
2
3
4
5
switch ((1, 2)) {
// 'var a' 和 'var b' 是将分别绑定到 1 和 2 的变量模式。
case (var a, var b): // ...
// 'a' 和 'b' 在 case 主体中有效。
}
类型化变量模式只有在匹配的值具有声明的类型时才匹配,否则失败:
1
2
3
4
switch ((1, 2)) {
// 不匹配。
case (int a, String b): // ...
}
## 标识符 定义为:已声明的变量,_, 它主要用来匹配变量的值,如下所示:
1
2
3
4
5
6
7
const c = 1;
switch (2) {
case c:
print('match $c');
default:
print('no match'); // 打印“no match”。
}
在这里,c 是一个变量,当匹配对象的值与 c 变量相等时,匹配成功。

列表

定义为:[subpattern1, subpattern2], 列表模式匹配List 对象的形状和值,并解构出元素内容。

1
2
3
4
5
6
7
8
const a = 'a';
const b = 'b';
switch (obj) {
// 列表模式 [a, b] 首先匹配 obj,如果 obj 是一个具有两个字段的列表,
// 然后如果它的字段匹配常量子模式 'a' 和 'b'。
case [a, b]:
print('$a, $b');
}

列表模式要求模式中的元素数量与整个列表匹配。但是,您可以使用 剩余元素 作为占位符来解释列表中任意数量的元素。在列表模式可以包含 一个 剩余元素 ...,它允许匹配任意长度的列表。

1
2
3
var [a, b, ..., c, d] = [1, 2, 3, 4, 5, 6, 7];
// 打印“1 2 6 7”。
print('$a $b $c $d');

映射

定义为:{"key": subpattern1, someConst: subpattern2},映射模式匹配 Map 对象,然后递归地将它的子模式与映射的键进行匹配以对其进行解构。 映射模式不要求模式匹配整个映射。映射模式会忽略映射包含的任何未被模式匹配的键。

1
2
3
4
if (json case {'user': [String name, int age]}) {
print('User $name is $age years old.');
}

case 模式同时验证: - json 是一个映射,因为它必须首先匹配外部映射模式才能继续(即匹配 Map 对象)。并且,由于它是一个映射,它也确认 json 不为 null。 - json 包含一个键 user 。 - 键 user 与一个包含两个值的列表配对。 - 列表值的类型为 String 和 int 。 - 用于保存值的新的局部变量为 name 和 age 。

记录

定义为:(subpattern1, subpattern2)(x: subpattern1, y: subpattern2),记录模式匹配记录对象并解构其字段。如果值不是与模式具有相同 形状 的记录,则匹配失败。否则,字段子模式将与记录中相应的字段进行匹配。 记录模式要求模式匹配整个记录。要使用模式解构具有命名字段的记录,请在模式中包含字段名称:

1
var (myString: foo, myNumber: bar) = (myString: 'string', myNumber: 1);
getter名称可以省略,并从 变量模式 推断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用变量子模式的记录模式:
var (untyped: untyped, typed: int typed) = record;
var (:untyped, :int typed) = record;

switch (record) {
case (untyped: var untyped, typed: int typed): // ...
case (:var untyped, :int typed): // ...
}

// 使用空检查和空断言子模式的记录模式:
switch (record) {
case (checked: var checked?, asserted: var asserted!): // ...
case (:var checked?, :var asserted!): // ...
}

// 使用强制类型转换子模式的记录模式:
var (untyped: untyped as int, typed: typed as String) = record;
var (:untyped as int, :typed as String) = record;

对象

定义为:SomeClass(x: subpattern1, y: subpattern2),对象模式使用对象属性上的 getter 来检查匹配的值与给定的命名类型是否匹配以解构数据。如果值没有相同的类型,则匹配失败。

1
2
3
4
switch (shape) {
// 如果 shape 的类型为 Rect,则匹配,然后与 Rect 的属性进行匹配。
case Rect(width: var w, height: var h): // ...
}

getter名称可以省略,并从字段子模式中的 变量模式标识符模式 推断:

1
2
// 将新的变量 x 和 y 绑定到 Point 的 x 和 y 属性的值。
var Point(:x, :y) = Point(1, 2);

对象模式不要求模式匹配整个对象。如果对象具有模式未解构的额外字段,它仍然可以匹配。

通配符

定义为:_,名为 _ 的模式是一个通配符,它是一个 变量模式标识符模式 ,它不绑定或分配给任何变量。

1
2
var list = [1, 2, 3];
var [_, two, _] = list;
当您想要测试值的类型但不将值绑定到名称时,带有类型注解的通配符名称很有用:
1
2
3
4
switch (record) {
case (int _, String _):
print('First field is int and second is String.');
}

函数

命名参数

在 Dart 中,可以对参数进行命名,命名参数是可选的,如是是必选的,则需要用 required 标记。 定义函数时,使用 {param1, param2, …} 指定命名参数:

1
2
/// 设置 [bold] 和 [hidden] 标志 ...
void enableFlags({bool? bold, bool? hidden}) {...}
调用函数时,可以使用 paramName: value 指定命名参数:
1
enableFlags(bold: true, hidden: false);
可以使用 = 指定默认值,指定的值必须是编译时常量。
1
2
3
4
5
/// 设置 [bold] 和 [hidden] 标志 ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold 将为 true;hidden 将为 false。
enableFlags(bold: true);
如果命名参数是必需的,要求调用者为该参数提供值,需要使用 required 对其进行标记:
1
const Scrollbar({super.key, required Widget child});
如果尝试在不指定 child 参数的情况下创建 Scrollbar ,则编译器会报错。

可选位置参数

将一组函数参数括在 [] 中将其标记为可选位置参数。如果不提供默认值,则它们的类型必须是可空的,它们的默认值为 null :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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');
另外,也可以使用 = 指定默认值。指定的值必须是编译时常量。
1
2
3
4
5
6
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');

函数类型

通过用关键字 Function 来定义函数的类型。

1
2
3
4
5
6
void greet(String name, {String greeting = 'Hello'}) =>
print('$greeting $name!');

// 将 `greet` 存储在变量中并调用它。
void Function(String, {String greeting}) g = greet;
g('Dash', greeting: 'Howdy');

箭头函数

=> expr 语法是 { return expr; } 的简写。 => 符号被称为箭头语法。

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

Dart 是一种面向对象的语言,有两种继承方式:类继承和mixin 继承。每个对象都是一个类的实例,除 Null 外的所有类都继承自 Object

基本用法

类成员和方法

Java 语言类型,Dart 对象具有属性和方法,使用 .来进行引用。

1
2
3
4
5
6
7
var p = Point(2, 2);

// 获取 y 的值。
assert(p.y == 2);

// 在 p 上调用 distanceTo()。
double distance = p.distanceTo(Point(4, 4));

如果对象有可能是 Null, 可以使用 ?. 来避免异常。

1
2
// 如果 p 不为 null,则将一个变量设置为其 y 值。
var a = p?.y;

使用构造函数

可以使用 构造函数 来创建对象,构造函数 有两种形式: ClassName 或 ClassName.identifier,如下所示:

1
2
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
也可以加上 new 关键字。
1
2
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

获取对象的类型

要在运行时获取对象的类型,您可以使用 Object 属性 runtimeType ,它返回一个 Type 对象。

1
print('a 的类型是 ${a.runtimeType}');

实例变量

以下是声明实例变量的方法:

1
2
3
4
5
6
class Point {
double? x; // 声明实例变量 x,初始值为 null。
double? y; // 声明 y,初始值为 null。
double z = 0; // 声明 z,初始值为 0。
late double? w; // 声明实例变量 w, 实例化之后赋值 。
}

使用 可空类型 声明的未初始化实例变量的值为 null 。不可空实例变量必须在声明时初始化 。

所有实例变量都会生成一个隐式 getter 方法。没有初始值的非 final 实例变量和 late final 实例变量还会生成一个隐式 setter 方法。

类变量和方法

使用 static 关键字实现类变量和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'dart:math';

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

static const initialCapacity = 16;

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

构造函数

构造函数是创建类实例的特殊函数。

Dart 实现多种类型的构造函数。 除了默认构造函数外, 这些函数使用与其类相同的名称。

  • 生成式构造函数 :创建新实例并初始化实例变量。
  • 默认构造函数 :不带参数的构造函数。
  • 命名构造函数 :带有特定功能的构造函数,如使用 Json 来构造对象。
  • 常量构造函数 :创建编译时常量的实例。
  • 工厂构造函数 :创建子类型的新的实例或从缓存中返回现有实例。
  • 重定向构造函数 :将调用转发到同一类中的另一个构造函数。

生成式构造函数

初始化实例变量的构造函数,如下所示:

1
2
3
4
5
6
7
8
class Point {
// 实例变量,用于保存点的坐标。
double x;
double y;

// 带有初始化形式参数的生成式构造函数:
Point(this.x, this.y);
}

默认构造函数

如果未声明构造函数,Dart 将使用默认构造函数。 默认构造函数是一个不带参数或名称的生成式构造函数。

命名构造函数

可以为构造函数指定特定的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const double xOrigin = 0;
const double yOrigin = 0;

class Point {
final double x;
final double y;

// 设置 x 和 y 实例变量
// 在构造函数体运行之前。
Point(this.x, this.y);

// 命名构造函数
Point.origin()
: x = xOrigin,
y = yOrigin;
}

常量构造函数

要将对象设为编译时常量,可以定义一个 const 构造函数,并将所有实例变量设置为 final。

1
2
3
4
5
6
7
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);

final double x, y;

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

重定向构造函数

构造函数可能会重定向到同一类中的另一个构造函数。重定向构造函数具有空主体。 构造函数在冒号 (:) 后使用 this 而不是类名。

1
2
3
4
5
6
7
8
9
class Point {
double x, y;

// 此类的主要构造函数。
Point(this.x, this.y);

// 委托给主构造函数。
Point.alongXAxis(double x) : this(x, 0);
}

工厂构造函数

当遇到以下两种情况时, 可以使用工厂构造函数,它使用 factory 关键字:

  • 从缓存中获取对象的实例;
  • 在构造对象之前需要执行一些复杂的操作。

以下示例包含两个工厂构造函数:

  • Logger 工厂构造函数从缓存中返回对象;
  • Logger.fromJson 工厂构造函数从 JSON 对象初始化 final 变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Logger {
final String name;
bool mute = false;

// _cache 是库私有的,这要感谢其名称前面的 _。
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);
}
}

像任何其他构造函数一样使用工厂构造函数:

1
2
3
4
5
var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

重定向构造函数

重定向工厂构造函数可以重定向为另一个类的构造函数。

1
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable

构造函数 tear-offs

类似于 Java 中的方法引用,可以直接使用构造函数的名称来完成调用,从而简化代码。

1
2
3
4
5
// 为命名构造函数使用 tear-offs:
var strings = charCodes.map(String.fromCharCode);

// 为未命名构造函数使用 tear-offs:
var buffers = charCodes.map(StringBuffer.new);

等同于下面的代码:

1
2
3
4
5
// 不是为命名构造函数使用 lambda:
var strings = charCodes.map((code) => String.fromCharCode(code));

// 不是为未命名构造函数使用 lambda:
var buffers = charCodes.map((code) => StringBuffer(code));

实例变量初始化

Dart 可以通过三种方式初始化变量。

在声明中初始化实例变量

声明变量时初始化实例变量。

1
2
3
4
5
6
7
8
9
10
11
12
class PointA {
double x = 1.0;
double y = 2.0;

// 隐式默认构造函数将这些变量设置为 (1.0,2.0)
// PointA();

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

使用初始化形式参数

Dart 构造函数中,可以使用 this.<propertyName> 来简化变量的初始化,并可以省略函数体。

1
2
3
4
5
6
7
8
9
10
11
class PointB {
final double x;
final double y;

// 设置 x 和 y 实例变量
// 在构造函数体运行之前。
PointB(this.x, this.y);

// 初始化形式参数也可以是可选的。
PointB.optional([this.x = 0.0, this.y = 0.0]);
}

私有字段不能用作命名的初始化形式参数:

1
2
3
4
5
6
7
8
9
class PointB {
// ...

PointB.namedPrivate({required double x, required double y})
: _x = x,
_y = y;

// ...
}

也可以与命名变量一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PointC {
double x; // 必须在构造函数中设置
double y; // 必须在构造函数中设置

// 带有默认值的初始化形式参数的生成式构造函数
PointC.named({this.x = 1.0, this.y = 1.0});

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

// 使用命名变量的构造函数。
final pointC = PointC.named(x: 2.0, y: 2.0);

使用初始化列表

类似 C++ 语言,使用初始化列表来初始化参数:

1
2
3
4
5
6
// 初始化列表在构造函数体运行之前设置实例变量。
Point.fromJson(Map<String, double> json)
: x = json['x']!,
y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}

! 是空断言,如果值不为空则赋值,如果为空则抛出异常。

构造函数继承

Dart 对象之间存在继承关系时,按以下顺序执行构造函数:

  1. 初始化列表;
  2. 超类的无名、无参构造函数;
  3. 主类的无参构造函数。

如果超类缺少无名、无参数的构造函数, 则调用超类中的一个构造函数。 在构造函数体(如果有)之前, 在冒号 (:) 后指定超类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
String? firstName;

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

class Employee extends Person {
// Person 没有默认构造函数;
// 您必须调用 super.fromJson()。
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}

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

超参数

为了避免将每个参数传递到构造函数的超调用中, 使用超初始化参数将参数转发到指定的或默认的超类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vector2d {
final double x;
final double y;

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

class Vector3d extends Vector2d {
final double z;

// 将 x 和 y 参数转发到默认的超构造函数,例如:
// Vector3d(final double x, final double y, this.z) : super(x, y);
Vector3d(super.x, super.y, this.z);
}

当超构造函数具有命名参数时,可以将它们拆分为命名超参数(在下一个示例中为 super.y ) 和超构造函数调用的命名参数( super.named(x: 0) )。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Vector2d {
// ...
Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
final double z;

// 将 y 参数转发到命名的超构造函数,例如:
// 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);
}

方法

方法是对象提供的函数。

实例方法

同其它语言一样,实例方法可以使用 this来进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:math';

class Point {
final double x;
final double y;

// 在构造函数体运行之前设置 x 和 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);
}
}

运行符

C++ 语言相似,Dart 可以提供对运算符的支持,从而使直接在表达中运用对象。支持的运算符有:

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

使用 内置标识符 operator 就可以定义对象支持的运算符,下面的例子定义了向量加法( + )、减法( - )和相等性( == ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 是对象属性的读写访问权限的特殊方法,可以使用 getset 关键字实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Rectangle {
double left, top, width, height;

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

// 定义两个计算属性:right 和 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);
}

left, top, width, height 四个属性默认生成了 GetterSetter 方法,不用显式地定义。另外定义了名为 rightbottomGetterSetter 方法,它们由其它属性计算得到。

抽象方法

如果需要定义由子类来实现的方法,就可以使用 抽象方法。抽象方法只能存在于 抽象类mixin 中,其方法体为空:

1
2
3
4
5
6
7
8
9
10
11
abstract class Doer {
// 定义实例变量和方法...

void doSomething(); // 定义一个抽象方法。
}

class EffectiveDoer extends Doer {
void doSomething() {
// 提供实现,因此此处方法不是抽象的...
}
}

类继承

Java 语言一样,类可以继承父类的属性和方法,使用 extends 关键字来表示继承,使用 super 引用超类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}

class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}

子类可以重写实例方法(包括运算符 )、gettersetter,可以使用 @override 注解来表示重写成员。

1
2
3
4
5
6
7
8
9
10
class Television {
// ···
set contrast(int value) {...}
}

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

隐式接口

使用 extend 只能实现单继承,要实现 JavaC++ 中的多继承,可以使用 implements 关键字。不同于其它语言,在 Dart 中没有单独的接口类,如 Java 中的 interface,它使用了一种独特的方式来实现接口。

Dart 中,每一个类都是一个隐式的接口,将 extend 改为 implements 即可。相对于 extend,隐式接口要求子类实现所有的方法,包括 Getter 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 一个人的。隐式接口包含 greet()。
class Person {
// 在接口中,但仅在此库中可见。
final String _name;

// 不在接口中,因为这是一个构造函数。
Person(this._name);

// 在接口中。
String greet(String who) => 'Hello, $who. I am $_name.';
}

// Person 接口的实现。
class Impostor implements Person {
// 实现 _name 的 getter 方法
@override
String get _name => '';

// 实现 greet 方法。
@override
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()));
}

Mixin

MixinDart 中独有的概念,它也可以实现多继承。另外,隐式接口 要求子类实现接口中所有的方法,而 Mixin 可以保留自己的方法实现。从概念来说,隐式接口 实现了 is-a 的关系,而 Mixin 更偏向于 has-a 的关系。整体来说,要实现代码复用及组合的功能,使用 Mixin 更合适。

Mixin 类使用 mixin 关键字来定义,子类要继承 Mixin 类,使用 with 关键字,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mixin MoveAble{
void move(){
print("=====$runtimeType move====");
}
}

mixin PaintAble{
void paint(){
print("=====$runtimeType paint====");
}
}

class Shape with MoveAble,PaintAble{
}

在上面的代码中,Shape 继承了 MoveAblePaintAble 两个 Mixin 类,直接就可以使用 movepaint 方法。如果使用 implements 则需要子类实现这两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MoveAble{
void move(){
print("=====$runtimeType move====");
}
}

class PaintAble{
void paint(){
print("=====$runtimeType paint====");
}
}

class Shape implements MoveAble,PaintAble{

@override
void move(){
print("=====$runtimeType move====");
}

@override
void move(){
print("=====$runtimeType move====");
}
}

在这中场景下,使用接口则显得比较笨拙。

说明:在 Mixin 类中不能包含构造方法,即不能被实例化。

Mixin 类中,也可以有继承关系,它使用关键字 on 来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mixin Musician {
musicianMethod() {
print('Playing music!');
}
}

mixin MusicalPerformer on Musician {
performerMethod() {
print('Performing music!');
super.musicianMethod();
}
}

class SingerDancer with Musician,MusicalPerformer { }

main() {
SingerDancer().performerMethod();
}

SingerDancer 类继承了 MusicalPerformer类,而 MusicalPerformer类继承了 Musician 类,要求在SingerDancer 类的说明中,包括 Musician,即要求指定 Mixin 类上继承链上所有的类。

----- 未完待续 -----

参考:


1. Dart 官方文档

2. Flutter 语法进阶 | 深入理解混入类 mixin

3. Flutter 语法进阶 | 抽象类和接口本质的区别