Dart 特殊语法
Dart 是一种由 Google 开发的面向对象的编程语言,主要用于构建跨平台应用。相对于其它开发语言,它有一些独特的语法和功能,这篇文章将这些功能记录如下,以方便查阅。
变量
在 Dart 语言中,可以使用 var
声明变量,而无需显示指定它们的类型,这些变量的类型由它们的初始值确定:
1 | var name = 'Voyager I'; |
另外,也可以显示声明变量的类型: 1
2// name 的类型为 String
String name = 'Bob';
空安全
默认情况下,声明变量时,变量不允许为空,在使用前必须赋值,如果没有赋值,则编译器会报错,禁止访问非空变量。如果这个变量可以为空,则需要在类型声明的末尾添加
?:
1 | String? name; // 可空类型。可以是 `null` 或字符串。 |
延迟变量
可以使用 late
声明一个变量为延迟变量,它主要用在声明该变量时其值还不能确定或初始化成本很高的场景,需要延迟赋值。如下所示:
1
late String description;
final 和 const
如果变量的值初始化之后就会再改变,可以使用 final 或
const 来定义。其中,final
变量表示只能设置一次,一般是初始后之后将不再改变,而const
变量是编译时常量,在编译时就要确定变量的值。
1 | // 初始化之后将不同改变 |
运算符
赋值运算符
常规的赋值运算符为 =,如果要仅在被赋值变量为
null 时赋值,请使用 ??= 运行符。
1
2
3
4// 将值赋给 a
a = value;
// 如果 b 为 null,则将值赋给 b;否则,b 保持不变
b ??= value;
条件表达式
Dart 有两个运算符:
如果
condition为true, 返回 expr1,否则返回 exp2.1
condition ? expr1 : expr2
如果
expr1不为 null, 则返回其值;否则,返回 exp2.1
expr1 ?? expr2
实例: 1
2
3var 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');
记录的类型
记录的类型使用逗号分隔的括在括号中的类型列表来表示,如
(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
6var 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
8var list = [1, 2, 3];
// 可以在最后添加一个逗号
var list = [
'Car',
'Boat',
'Plane',
];
列表使用索引进行访问,其中 0 是第一个值的索引,
list.length - 1 是最后一个值的索引。您可以使用
.length 属性获取列表的长度,并使用下标运算符
([]) 访问列表的值。
1 | var list = [1, 2, 3]; |
集合
Dart 使用集合 Set
来表示无序的集合,要创建一个空集合,请使用以类型参数为前缀的
{} ,或将 {} 分配给 Set
类型的变量。
1 | // 定义一个简单的集合 |
使用 add() 或 addAll()
方法向现有集合中添加项目:
1 | var elements = <String>{}; |
使用 .length 获取集合中项目的数量:
1 | var elements = <String>{}; |
映射
映射就是常见的 Map 键和值,定义如下: 1
2
3
4
5
6
7
8
9
10
11
12var gifts = {
// 键:值
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};[] 添加和访问映射键值对。
1 | var gifts = {'first': 'partridge'}; |
如果您查找映射中不存在的键,则会返回 null :
1 | var gifts = {'first': 'partridge'}; |
使用 .length 获取映射中键值对的数量:
1 | var gifts = {'first': 'partridge'}; |
运算符
展开运算符
Dart 在列表、映射和集合中支持展开运算符 (...)
和空感知展开运算符
(...?)。展开运算符提供了一种简洁的方式将多个值插入集合中。
可以使用展开运算符 (...) 将列表的所有值插入另一个列表中:
1
2
3var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
如果展开运算符右侧的表达式可能为 null,则可以使用空感知展开运算符
(...?) 来避免异常: 1
2var list2 = [0, ...?list];
assert(list2.length == 1);
控制流运算符
Dart 提供集合 if 和 for
用于列表、映射和集合变量。可以使用这些运算符使用条件语句
(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
3var listOfInts = [1, 2, 3];
var listOfStrings = ['#0', for (var i in listOfInts) '#$i'];
assert(listOfStrings[1] == '#1');
模式
模式类似于其它语言中的
正则表达式,它主要有两个作用,分别是
匹配和解构。匹配主要是根据上下文和模式的形状来进行判断
模式 与 匹配对象
是否匹配。而解构则是从 匹配对象
中取出对应的值。
模式匹配主要从下面几个方面判断 模式 与
给定值(匹配对象)是否匹配:
- 具有某种形状;
- 是某个常量;
- 等于其他内容;
- 具有某种类型。
模式匹配等同于返回一个 bool 值,可以在
if,switch 和 for
语句中使用,如下所示: 1
2
3
4
5
6
7const a = 'a';
const b = 'b';
switch (obj) {
// 如果 obj 是一个包含两个字段的列表,则列表模式 [a, b] 首先匹配 obj,然后如果其字段匹配常量子模式 'a' 和 'b'。
case [a, b]:
print('$a, $b');
}
当对象和模式匹配时,模式可以访问对象的数据并将其分配给指定的变量,这便是
解构 对象。解构 对象需要定义变量,如下所示:
1
2
3
4
5var numList = [1, 2, 3];
// 列表模式 [a, b, c] 解构 numList 中的三个元素...
var [a, b, c] = numList;
// ...并将它们分配给新的变量。
print(a + b + c);var 或 final
开头,后跟一个模式。
模式有几种主要类型,下面进行分析。
空检查
定义为: subpattern?,如果值不为 null,
模式匹配成功,模式中同时可以给定一个变量,即
解构变量,如果值为空,则匹配失败。 1
2
3
4
5String? maybeString = 'nullable with base type String';
switch (maybeString) {
case var s?:
// 's' 在此处具有非空 String 类型。
}
空断言
定义为:subpattern!,
如果对象不为空,则进行匹配和解构,如果为空,则抛出异常。
1
2
3
4
5List<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
4switch (number) {
// 如果 1 == number,则匹配。
case 1: // ...
}
变量
定义为:var bar, String str, final int _,
变量模式将新变量绑定到匹配的值上,它们主要是获取对象上的值。
1
2
3
4
5switch ((1, 2)) {
// 'var a' 和 'var b' 是将分别绑定到 1 和 2 的变量模式。
case (var a, var b): // ...
// 'a' 和 'b' 在 case 主体中有效。
}1
2
3
4switch ((1, 2)) {
// 不匹配。
case (int a, String b): // ...
}已声明的变量,_,
它主要用来匹配变量的值,如下所示: 1
2
3
4
5
6
7const c = 1;
switch (2) {
case c:
print('match $c');
default:
print('no match'); // 打印“no match”。
}c
是一个变量,当匹配对象的值与 c 变量相等时,匹配成功。
列表
定义为:[subpattern1, subpattern2],
列表模式匹配List 对象的形状和值,并解构出元素内容。
1 | const a = 'a'; |
列表模式要求模式中的元素数量与整个列表匹配。但是,您可以使用
剩余元素
作为占位符来解释列表中任意数量的元素。在列表模式可以包含 一个 剩余元素
...,它允许匹配任意长度的列表。 1
2
3var [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
4if (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);
变量模式 推断。
1 | // 使用变量子模式的记录模式: |
对象
定义为:SomeClass(x: subpattern1, y: subpattern2),对象模式使用对象属性上的
getter
来检查匹配的值与给定的命名类型是否匹配以解构数据。如果值没有相同的类型,则匹配失败。
1 | switch (shape) { |
getter名称可以省略,并从字段子模式中的
变量模式 或 标识符模式 推断:
1
2// 将新的变量 x 和 y 绑定到 Point 的 x 和 y 属性的值。
var Point(:x, :y) = Point(1, 2);
对象模式不要求模式匹配整个对象。如果对象具有模式未解构的额外字段,它仍然可以匹配。
通配符
定义为:_,名为 _
的模式是一个通配符,它是一个 变量模式 或
标识符模式 ,它不绑定或分配给任何变量。 1
2var list = [1, 2, 3];
var [_, two, _] = list;1
2
3
4switch (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});
可选位置参数
将一组函数参数括在 []
中将其标记为可选位置参数。如果不提供默认值,则它们的类型必须是可空的,它们的默认值为
null : 1
2
3
4
5
6
7
8
9
10
11
12
13
14String 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
6String 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
6void 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 | var p = Point(2, 2); |
如果对象有可能是 Null, 可以使用 ?.
来避免异常。
1 | // 如果 p 不为 null,则将一个变量设置为其 y 值。 |
使用构造函数
可以使用 构造函数 来创建对象,构造函数
有两种形式: ClassName 或 ClassName.identifier,如下所示:
1
2var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});new 关键字。 1
2var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
获取对象的类型
要在运行时获取对象的类型,您可以使用 Object 属性
runtimeType ,它返回一个 Type 对象。
1
print('a 的类型是 ${a.runtimeType}');
实例变量
以下是声明实例变量的方法:
1 | class Point { |
使用 可空类型 声明的未初始化实例变量的值为
null 。不可空实例变量必须在声明时初始化 。
所有实例变量都会生成一个隐式 getter 方法。没有初始值的非
final 实例变量和 late final
实例变量还会生成一个隐式 setter 方法。
类变量和方法
使用 static 关键字实现类变量和方法。
1 | import 'dart:math'; |
构造函数
构造函数是创建类实例的特殊函数。
Dart 实现多种类型的构造函数。 除了默认构造函数外, 这些函数使用与其类相同的名称。
- 生成式构造函数 :创建新实例并初始化实例变量。
- 默认构造函数 :不带参数的构造函数。
- 命名构造函数 :带有特定功能的构造函数,如使用 Json 来构造对象。
- 常量构造函数 :创建编译时常量的实例。
- 工厂构造函数 :创建子类型的新的实例或从缓存中返回现有实例。
- 重定向构造函数 :将调用转发到同一类中的另一个构造函数。
生成式构造函数
初始化实例变量的构造函数,如下所示: 1
2
3
4
5
6
7
8class 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
16const 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 | class ImmutablePoint { |
重定向构造函数
构造函数可能会重定向到同一类中的另一个构造函数。重定向构造函数具有空主体。 构造函数在冒号 (:) 后使用 this 而不是类名。
1 | class Point { |
工厂构造函数
当遇到以下两种情况时, 可以使用工厂构造函数,它使用 factory 关键字:
- 从缓存中获取对象的实例;
- 在构造对象之前需要执行一些复杂的操作。
以下示例包含两个工厂构造函数:
- Logger 工厂构造函数从缓存中返回对象;
- Logger.fromJson 工厂构造函数从 JSON 对象初始化 final 变量。
1 | class Logger { |
像任何其他构造函数一样使用工厂构造函数:
1 | var logger = Logger('UI'); |
重定向构造函数
重定向工厂构造函数可以重定向为另一个类的构造函数。
1 | factory Listenable.merge(List<Listenable> listenables) = _MergingListenable |
构造函数 tear-offs
类似于 Java
中的方法引用,可以直接使用构造函数的名称来完成调用,从而简化代码。
1 | // 为命名构造函数使用 tear-offs: |
等同于下面的代码:
1 | // 不是为命名构造函数使用 lambda: |
实例变量初始化
Dart 可以通过三种方式初始化变量。
在声明中初始化实例变量
声明变量时初始化实例变量。
1 | class PointA { |
使用初始化形式参数
在 Dart 构造函数中,可以使用
this.<propertyName>
来简化变量的初始化,并可以省略函数体。
1 | class PointB { |
私有字段不能用作命名的初始化形式参数:
1 | class PointB { |
也可以与命名变量一起使用:
1 | class PointC { |
使用初始化列表
类似 C++ 语言,使用初始化列表来初始化参数:
1 | // 初始化列表在构造函数体运行之前设置实例变量。 |
! 是空断言,如果值不为空则赋值,如果为空则抛出异常。
构造函数继承
Dart
对象之间存在继承关系时,按以下顺序执行构造函数:
- 初始化列表;
- 超类的无名、无参构造函数;
- 主类的无参构造函数。
如果超类缺少无名、无参数的构造函数, 则调用超类中的一个构造函数。 在构造函数体(如果有)之前, 在冒号 (:) 后指定超类构造函数。
1 | class Person { |
超参数
为了避免将每个参数传递到构造函数的超调用中, 使用超初始化参数将参数转发到指定的或默认的超类构造函数。
1 | class Vector2d { |
当超构造函数具有命名参数时,可以将它们拆分为命名超参数(在下一个示例中为
super.y ) 和超构造函数调用的命名参数(
super.named(x: 0) )。
1 | class Vector2d { |
方法
方法是对象提供的函数。
实例方法
同其它语言一样,实例方法可以使用 this来进行访问。
1 | import 'dart:math'; |
运行符
同 C++ 语言相似,Dart
可以提供对运算符的支持,从而使直接在表达中运用对象。支持的运算符有:
| < | > | <= | >= | == | ~ |
| - | + | / | ~/ | * | % |
| |
ˆ |
& |
<< | >>> | >> |
| []= | [] |
使用 内置标识符 operator
就可以定义对象支持的运算符,下面的例子定义了向量加法( + )、减法( -
)和相等性( == ):
1 | class Vector { |
Getter 和 Setter
Getter 和 Setter
是对象属性的读写访问权限的特殊方法,可以使用 get 和
set 关键字实现:
1 | class Rectangle { |
left, top, width,
height 四个属性默认生成了 Getter 和
Setter 方法,不用显式地定义。另外定义了名为
right 和 bottom 的 Getter 和
Setter 方法,它们由其它属性计算得到。
抽象方法
如果需要定义由子类来实现的方法,就可以使用
抽象方法。抽象方法只能存在于 抽象类 或
mixin 中,其方法体为空:
1 | abstract class Doer { |
类继承
与 Java 语言一样,类可以继承父类的属性和方法,使用
extends 关键字来表示继承,使用 super
引用超类:
1 | class Television { |
子类可以重写实例方法(包括运算符 )、getter
和 setter,可以使用 @override
注解来表示重写成员。
1 | class Television { |
隐式接口
使用 extend 只能实现单继承,要实现 Java 和
C++ 中的多继承,可以使用 implements
关键字。不同于其它语言,在 Dart 中没有单独的接口类,如
Java 中的
interface,它使用了一种独特的方式来实现接口。
在 Dart 中,每一个类都是一个隐式的接口,将
extend 改为 implements 即可。相对于
extend,隐式接口要求子类实现所有的方法,包括
Getter 方法。
1 | // 一个人的。隐式接口包含 greet()。 |
Mixin
Mixin 是 Dart
中独有的概念,它也可以实现多继承。另外,隐式接口
要求子类实现接口中所有的方法,而 Mixin
可以保留自己的方法实现。从概念来说,隐式接口 实现了
is-a 的关系,而 Mixin 更偏向于
has-a 的关系。整体来说,要实现代码复用及组合的功能,使用
Mixin 更合适。
Mixin 类使用 mixin 关键字来定义,子类要继承
Mixin 类,使用 with 关键字,如下代码所示:
1 | mixin MoveAble{ |
在上面的代码中,Shape 继承了 MoveAble 和
PaintAble 两个 Mixin 类,直接就可以使用
move 和 paint 方法。如果使用
implements 则需要子类实现这两个方法:
1 | class MoveAble{ |
在这中场景下,使用接口则显得比较笨拙。
说明:在
Mixin类中不能包含构造方法,即不能被实例化。
在 Mixin 类中,也可以有继承关系,它使用关键字
on 来定义。
1 | mixin Musician { |
SingerDancer 类继承了
MusicalPerformer类,而
MusicalPerformer类继承了 Musician
类,要求在SingerDancer 类的说明中,包括
Musician,即要求指定 Mixin
类上继承链上所有的类。
----- 未完待续 -----
参考: