本文基于官方的文档,整理出 Dart 语言中与 Java 和 Kotlin 语言类似和特有的部分,因此本文的读者需要具备一定的 Java 及 Kotlin 语言基础,相信大家读完本文就可以看懂大部分的 Flutter 代码了。
在学习 Dart 语言时,我们可以使用 DartPad 来编写和调试 Dart 的代码,体验 Dart 的大部分语言功能。
1. 概述
Dart 语言同时借鉴了 Java 和 JavaScript,它在静态语法方面和 Java 非常相似,如类型定义、函数声明、泛型等等,而在动态特性方面又和 JavaScript 很像,如函数式特性、异步支持等等。除了融合 Java 和 JavaScript 语言的优势之外,Dart 也具有一些其它具有表现力的语法,如可选命名参数、..(级联运算符)和 ?.(条件成员访问运算符)以及 ??(判空赋值运算符)等等。
2. 变量
2.1 变量定义
在创建变量时,我们可以使用常规的类型定义:
String name = 'Bob'
也可以使用 var
关键字进行创建:
var name = 'Bob'
上面示例中 name 变量的类型会被推断为 String,如果想不局限于单一的类型,可以指定为 dynamic
类型,如下:
dynamic name = 'Bob';
name = 1
未初始化的变量都会默认初始化为 null,即使是数字变量。
2.2 final 和 const
Dart 中没有 Kotlin 的 val
关键字,但是有 final
和 const
,两者的区别是 final
变量在其第一次使用的时候才被初始化,而 const
变量是一个编译时的常量,且必须在声明变量时赋值。示例代码:
final name = 'Bob'; // final 可以直接替代 var 关键字
final String nickname = 'Bobby'; // 也可以加在一个具体的类型前
const bar = 1000000; // 直接赋值
const double atm = 1.01325 * bar; // 可以利用其它 const 变量赋值
另外还有一个区别是 final
修饰的变量不能修改引用的对象,而 const
修饰的变量不仅不能修改引用的对象,甚至不能修改对象的属性,即它是一个不可变的对象。示例代码:
class Person {
var name;
Person(this.name);
}
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
void main() {
final person = Person('Bob');
person.name = 'Bobby'; // 没有问题
const point = ImmutablePoint(1, 1);
point.x = 2; // x 不能被重新赋值
}
通过上面的例子可以看到,你可以将构造函数声明为 const
的,这种类型的构造函数创建的对象代表的是一个不可改变的对象。如果你在构造函数前加上了 const
,那么要确保所有属性均为 final
或 const
的,例如上面代码中的 ImmutablePoint
类。
当一个变量被 const
修饰时,这个变量引用的类的构造函数一定是 const
的,即这个类是不可变的。
另外,const
关键字不仅可以放在等号左边用来定义常量,还可以放在等号右边用来定义常量值,该常量值可以赋给任何变量。示例代码:
var point = const ImmutablePoint(1, 1);
point = ImmutablePoint(2, 2);
上面的代码是没有问题的,不过这种定义方式通常用于修饰集合:
var array = [1, 2, 3];
array[1] = 10; // 没有问题
var constArray = const [1, 2, 3];
constArray[1] = 10; // 不能修改数组内的元素
const baz = []; // 等同于 `const baz = const []`
如上面代码所示,如果集合前加上 const
关键字,代表它是一个不可变的集合。
在等号的两边也可以同时使用 const
关键字,但是通常可以省略等号右边的,从 Dart 2 开始可以根据上下文推断出来:
const point = const ImmutablePoint(1, 1); // 等同于 `const point = ImmutablePoint(1, 1);`
最后,如果使用 const
修饰类中的成员变量,则必须加上 static
关键字,即 static const
,例如 ImmutablePoint
类中的 origin
变量。
3. 内置类型
3.1 数字类型
Dart 支持 int
、double
和 num
,int
和 double
都是 num
的子类。下面是字符串和数字之间转换的方式:
// String -> int
var one = int.parse('1');
// String -> double
var onePointOne = double.parse('1.1');
// int -> String
String oneAsString = 1.toString();
// double -> String
String piAsString = 3.14159.toStringAsFixed(2); // 保留 2 位小数,输出 3.14
3.2 字符串
可以使用单引号或者双引号来创建字符串:
var s1 = '使用单引号创建字符串字面量。';
var s2 = "双引号也可以用于创建字符串字面量。";
var s3 = '使用单引号创建字符串时可以使用斜杠来转义那些与单引号冲突的字符串:\'。';
var s4 = "而在双引号中则不需要使用转义与单引号冲突的字符串:'";
可以在字符串中以 ${表达式}
的形式使用表达式,使用方式和 Kotlin 是一样的,这里不再详述。
也可以使用 +
运算符将两个字符串连接为一个,也可以将多个字符串挨着放一起变为一个:
var s1 = '可以拼接'
'字符串'
"即使它们不在同一行。";
可以使用三个单引号或者三个双引号创建多行字符串:
var s1 = '''
你可以像这样创建多行字符串。
''';
var s2 = """这也是一个多行字符串。""";
在字符串前加上 r
作为前缀可以创建原生字符串,即不会被做任何处理(比如转义)的字符串:
var s = r'在原生字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。'
3.3 布尔类型
Dart 使用 bool
关键字表示布尔类型。
3.4 List
在 Dart 中数组由 List
对象表示,List 字面量与 JavaScript 中数组字面量是一样的,如下:
var list = [1, 2, 3];
你可以像 Java 中数组的用法来操纵 Dart 中 List 的元素。
如果想要创建一个编译时常量的 List,在 List 字面量前添加 const
关键字即可,如 2.2 节所述,示例代码:
var constantList = const [1, 2, 3];
constantList[1] = 1; // 会报错,不能修改数组
Dart 在 2.3 引入了 扩展操作符 ...
和 可空的扩展操作符 ...?
,它们提供了一种将多个元素插入集合的简洁方法。
例如,你可以使用扩展操作符 ...
将一个 List 中的所有元素插入到另一个 List 中:
var list = [1, 2, 3];
var list2 = [0, ...list];
如果扩展操作符右边可能为 null ,你可以使用可空的扩展操作符 ...?
来避免产生异常:
var list;
var list2 = [0, ...?list]; // list2 中只有一个元素 0
Dart 在 2.3 还引入了 Collection If 和 Collection For,在构建集合时,可以使用条件判断和循环,示例代码:
var nav = [
'Home',
'Furniture',
'Plants',
if (promoActive) 'Outlet'
];
var listOfInts = [1, 2, 3];
var listOfStrings = [
'#0',
for (var i in listOfInts) '#$i'
];
3.5 Set
下面是使用 Set 字面量来创建一个 Set 集合的方法:
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
Set 字面量与 List 字面量的区别是:Set 字面量用花括号 {}
表示,而 List 字面量用方括号 []
表示。
可以在 {}
前加上类型参数创建一个空的 Set,或者将 {}
赋值给一个 Set 类型的变量:
var names = <String>{}; // 类型 + {} 的形式创建 Set
Set<String> names = {}; // 声明类型变量的形式创建 Set
可以在 Set 字面量前添加 const
关键字创建一个 Set 编译时常量:
final constantSet = const {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
constantSet.add('helium'); // 会报错,不能修改集合
从 Dart 2.3 开始,Set 也可以像 List 一样支持使用扩展操作符(...
和 ...?
)以及 Collection If 和 Collection For 操作。
3.6 Map
Dart 中使用 Map 字面量来创建 Map:
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
你也可以使用 Map 的构造器来创建 Map:
var nobleGases = Map();
// 添加键值对
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';
可以在 {}
前加上类型参数创建一个空的 Map,或者将 {}
赋值给一个 Map 类型的变量:
var nobleGases = <int, String>{}; // 类型 + {} 的形式创建 Map
Map<int, String> nobleGases = {}; // 声明类型变量的形式创建 Map
下面有一个问题,如下面代码所示,如果我忘记了加类型参数,该变量是 Set 还是 Map 呢?
var nobleGases = {};
答案其实是 Map。因为先有的 Map 字面量语法,所以 {}
默认是 Map
类型。如果忘记在 {}
上注释类型或赋值到一个未声明类型的变量上,Dart 会创建一个类型为 Map<dynamic, dynamic>
的对象。
同 List,在一个 Map 字面量前添加 const
关键字可以创建一个 Map 编译时常量:
final constantMap = const {
2: 'helium',
10: 'neon',
18: 'argon',
};
constantMap[2] = 'Helium'; // 会报错,不能修改映射
从 Dart 2.3 开始,Map 可以像 List 一样支持使用扩展操作符(...
和 ...?
)以及 Collection If 和 Collection For 操作。
4. 函数
Dart 是一种真正面向对象的语言,所以函数也是一个对象并且类型为 Function
。下面是定义一个函数的例子:
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
如果函数体内只包含一个表达式,可以使用简写语法:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
语法 => 表达式
是 { return 表达式; }
的简写, =>
也称之为胖箭头语法。
注意,在 =>
与 ;
之间的只能是表达式,比如你不能将一个 if 语句放在里面,但是可以放置条件表达式。
函数可以有两种形式的参数:必要参数 和 可选参数。必要参数定义在参数列表的前面,可选参数则定义在必要参数的后面。可选参数可以是 命名的 或 位置的。下面来详述一下。
4.1 可选参数
可选参数分为命名参数和位置参数,可在参数列表中任选其一使用,但两者不能同时出现在参数列表中。
4.1.1 命名参数
定义函数时,可以使用 {param1, param2, …}
来指定命名参数:
void enableFlags({bool bold, bool hidden}) {...}
当你调用该函数时,需要使用 参数名: 参数值
的形式来指定命名参数:
enableFlags(bold: true, hidden: false);
虽然命名参数是可选参数的一种类型,但是你仍然可以使用 @required
注解来标识一个命名参数是必需的参数,此时调用者则必须为该参数提供一个值。例如:
void enableFlags({bool bold, @required bool hidden}) {...}
命名参数在 Flutter 中的应用还是蛮多的。
4.1.2 位置参数
可以使用 []
将一系列参数包裹起来作为位置参数:
String say(String from, String msg, [String device]) {...}
say('Bob', 'Howdy') // 不使用可选参数调用上述函数
say('Bob', 'Howdy', 'smoke signal') // 使用可选参数调用上述函数
4.1.3 默认参数值
可以用 =
为函数的命名参数和位置参数定义默认值,没有指定默认值的情况下默认值为 null
。示例代码:
void enableFlags({bool bold = false, bool hidden = false}) {...}
List 或 Map 同样也可以作为默认值,示例代码:
void doStuff(
{List<int> list = const [1, 2, 3],
Map<String, String> gifts = const {
'first': 'paper',
'second': 'cotton',
'third': 'leather'
}}) {
print('list: $list');
print('gifts: $gifts');
}
4.2 main() 函数
每个 Dart 程序都必须有一个 main()
顶级函数作为程序的入口,main()
函数返回值为 void
并且有一个 List<String>
类型的可选参数。
4.3 函数作为对象
和 Kotlin 一样,可以将函数作为参数传递给另一个函数。例如:
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
// 将 printElement 函数作为参数传递。
list.forEach(printElement);
也可以将函数赋值给一个变量,比如:
var loudify = (msg) => msg.toUpperCase();
print(loudify('hello'));
4.4 匿名函数(Lambda 表达式)
在 Dart 中,匿名函数(或 Lambda 表达式)的格式如下:
([[类型] 参数[, …]]) {
函数体;
};
下面代码定义了只有一个参数 item 且没有参数类型的匿名方法:
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
如果函数体内只有一行语句,可以使用胖箭头缩写法:
list.forEach((item) => print('${list.indexOf(item)}: $item'));
4.5 返回值
在 Dart 中,所有的函数都有返回值,最后没有返回语句的函数最后一行默认为执行了 return null;
。
5. 运算符
5.1 算术运算符
Dart 支持的常用的算术运算符与 Java 是一样的,唯一不同是除号 /
的结果是一个浮点数,而整除用 ~/
表示,示例代码:
print(5 / 2); // 输出 2.5
print(5 ~/ 2); // 输出 2
5.2 关系运算符
Dart 支持的关系运算符也是与 Java 一样的,有一个与 Kotlin 类似的是要判断两个对象是否表示相同的事物使用 ==
即可。如果是自定义的类,需要重写 ==
运算符和 hashCode
值。
而如果需要确定两个对象是否完全相同,可以使用 identical()
函数。示例代码如下:
class Person {
final String firstName, lastName;
Person(this.firstName, this.lastName);
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return person.firstName == firstName && person.lastName == lastName;
}
@override
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}
}
void main() {
Person person1, person2;
print(person1 == person2); // 两个都为 null,也相等
person1 = Person("Jimmy", "Sun");
person2 = Person('Jimmy', 'Sun');
print(person1 == person2); // 输出 true
print(identical(person1, person2)); // 输出 false
}
5.3 类型判断运算符
与 Kotlin 类似,Dart 使用 as
、is
和 is!
来判断对象类型,而且在用 is
判断之后 Dart 可以推断该类型。
5.4 赋值运算符
Dart 可以使用 =
来赋值,也可以使用 ??=
来为值为 null
的变量赋值:
a = value;
b ??= value; // 当且仅当 b 为 null 时才赋值
5.5 条件表达式
Dart 中有两个特殊的运算符可以用来替代 if-else 语句,其中一个是 Java 中的三元表达式:条件 ? 表达式 1 : 表达式 2
,另一个是类似于 Kotlin 中的 Elvis 操作符:表达式 1 ?? 表达式 2
,如果表达式 1 为非 null 则返回其值,否则返回表达式 2 的值。
5.6 级联运算符(..)
类似于 Kotlin 的作用域函数,级联运算符 ..
可以让你在同一个对象上连续调用多个对象的变量或方法。
querySelector('#confirm') // 获取对象
..text = 'Confirm' // 使用对象的成员
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));
级联运算符还可以嵌套,例如:
final addressBook = (AddressBookBuilder()
..name = 'jenny'
..email = 'jenny@example.com'
..phone = (PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();
5.7 条件访问运算符
Dart 也支持 Kotlin 中的安全调用操作符:?.
。
6. 流程控制语句
6.1 For 循环
在 Dart 中,除了支持标准的 for 循环,还对 List 和 Set 等实现了 Iterable 接口的类支持 for-in
形式的迭代:
var collection = [0, 1, 2];
for (var x in collection) {
print(x); // 0 1 2
}
6.2 Switch 和 Case
在 Dart 中,每一个非空的 case
子句都必须有一个 break
语句,也可以通过 continue
、throw
或者 return
来结束非空的 case
语句,否则就会报错。
但是,Dart 也支持空的 case
语句,允许它以 fall-through 的形式执行,示例代码:
var command = 'CLOSED';
switch (command) {
case 'CLOSED': // case 语句为空时的 fall-through 形式。
case 'NOW_CLOSED':
executeNowClosed(); // case 条件值为 CLOSED 和 NOW_CLOSED 时均会执行该语句。
break;
}
如果想要在非空的 case 语句中实现 fall-through 的形式,可以使用 continue
配合标签的方式实现:
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
executeClosed();
continue nowClosed; // 继续执行标签为 nowClosed 的 case 子句。
nowClosed:
case 'NOW_CLOSED':
executeNowClosed(); // case 条件值为 CLOSED 和 NOW_CLOSED 时均会执行该语句。
break;
}
其它的诸如 if-else
、while
、do-while
、break
和 continue
等流程控制语句和 Java 的用法一样,这里不再详述。
7. 异常
和 Kotlin 一样,Dart 的所有异常都是非必检的异常。
7.1 抛出异常
在 Dart 中可以将任何非 null 对象作为异常抛出而不局限于 Exception
或 Error
类,例如:
throw 'Out of llamas!'
7.2 捕获异常
在 Dart 中使用 on
或 catch
来捕获异常,使用 on
来指定异常类型,使用 catch
来捕获异常对象,两者可单独使用,也可同时使用。示例代码:
try {
throw Exception('Out of llamas!');
} on Exception { // 单独使用 on
print("error");
}
try {
throw Exception('Out of llamas!');
} catch (e) { // 单独使用 catch
print(e);
}
try {
throw Exception('Out of llamas!');
} on Exception catch (e) { // 同时使用 on 和 catch
print(e);
}
可以使用关键字 rethrow
将捕获的异常再次抛出:
try {
throw Exception('Out of llamas!');
} on Exception {
rethrow;
}
8. 类
8.1 构造函数
8.1.1 成员变量赋值
Dart 提供了一种特殊的语法糖来简化为成员变量赋值的过程:
class Point {
num x, y;
Point(this.x, this.y);
}
8.1.2 命名式构造函数
可以为一个类声明多个命名式构造函数来表达更明确的意图:
class Point {
num x, y;
Point(this.x, this.y);
// 命名式构造函数
Point.origin() {
x = 0;
y = 0;
}
}
构造函数是不能被继承的,子类同样也不能继承父类的命名式构造函数。
8.1.3 调用父类非默认构造函数
如果父类没有无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用 :
即可。示例代码:
class Employee extends Person {
final Map data;
Employee(String firstName, String lastName, this.data) : super(firstName, lastName);
}
class Employee extends Person {
final Map data;
Employee(String firstName, String lastName, this.data) : super(firstName, lastName) {
// ...
}
}
8.1.4 初始化列表
除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量,每个实例变量之间使用逗号分隔。示例代码:
class Employee extends Person {
final Map data;
Employee(String firstName, String lastName)
: data = {}, super(firstName, lastName) {
// ...
}
}
8.1.5 重定向构造函数
有时候类中的构造函数会调用类中其它的构造函数,该重定向构造函数没有函数体,只需在函数签名后使用 :
指定需要重定向到的其它构造函数即可:
class Point {
num x, y;
// 该类的主构造函数。
Point(this.x, this.y);
// 委托实现给主构造函数。
Point.alongXAxis(num x) : this(x, 0);
}
8.1.6 使用构造函数
从 Dart 2 开始,创建对象的 new
关键字是可选的了。
另外,当两个在构造函数名之前加 const
关键字来创建编译时常量,并且参数值一样时,这两个对象是同一个对象:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
print(identical(a, b)); // 输出 true
8.2 获取对象的类型
可以使用 Object 对象的 runtimeType
属性在运行时获取一个对象的类型,该对象类型是 Type
类的实例:
print('The type of a is ${a.runtimeType}');
8.3 Getter 和 Setter 方法
实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,你也可以使用 get
和 set
关键字为额外的属性添加 Getter 和 Setter 方法:
class Rectangle {
num left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算产生的属性:right 和 bottom。
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
8.4 抽象类与隐式接口
Dart 中没有 interface
关键字,但是可以使用 abstract
关键字声明抽象类。
其实每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的成员变量和方法。如果想要创建一个 A 类支持调用 B 类的变量和方法并且不想继承 B 类,那么我们可以将 B 类作为接口进行实现。示例代码:
// Person 类的隐式接口中包含 greet() 方法。
class Person {
// _name 变量同样包含在接口中,但它只是库内可见的。
final _name;
// 构造函数不在接口中。
Person(this._name);
// greet() 方法在接口中。
String greet(String who) => '你好,$who。我是$_name。';
}
// Person 接口的一个实现。
class Impostor implements Person {
get _name => '';
String greet(String who) => '你好$who。你知道我是谁吗?';
}
8.5 扩展方法
可以使用 extension on
关键字来创建扩展方法,示例代码:
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
print('123'.parseInt() == 123); // 输出 true
9. 泛型
Dart 的泛型类型与 Java 一致,唯一不同的是它在运行时也会保持类型信息,即不会被擦除:
var names = List<String>();
names.addAll(['小芸', '小芳', '小民']);
print(names is List<String>); // true
10. 库和可见性
每个 Dart 程序都是一个代码库。
Dart 中没有类似于 Java 的 public、protected 和 private 成员访问限定符。如果一个标识符以下划线 _
开头则表示该标识符在代码库内是私有的。
11. 异步支持
11.1 处理 Future
在 Dart 中使用 async
和 await
关键字实现异步编程,它和 JavaScript 中的用法一模一样,并且让你的代码看起来就像是同步的一样。例如,下面的代码使用 await
等待异步函数的执行结果:
await lookUpVersion();
必须在带有 async
关键字的异步函数中才能使用 await
:
Future checkVersion() async {
var version = await lookUpVersion();
// 使用 version 继续处理逻辑
}
尽管异步函数可以处理耗时操作,但是它并不会等待这些耗时操作完成,异步函数执行时会在其遇到第一个 await
表达式的时候返回一个 Future
对象,然后等待 await
表达式执行完毕后继续执行。
可以使用 try-catch
处理 await
导致的异常:
try {
version = await lookUpVersion();
} catch (e) {
// 无法找到版本时做出的反应
}
也可以在异步函数中多次使用 await
关键字,示例代码:
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
await
表达式的返回值通常是一个 Future 对象;如果不是的话也会自动将其包裹在一个 Future
对象里。Future
对象代表一个“承诺”,即 await
表达式会阻塞直到需要的对象返回。
11.2 声明异步函数
定义异步函数时,需要将关键字 async
添加到函数并让其返回一个 Future
对象。假设有如下返回 String 对象的方法:
String lookUpVersion() => '1.0.0';
将其改为异步函数,返回值是 Future
:
Future<String> lookUpVersion() async => '1.0.0';
注意,使用 async
时函数体不需要使用 Future API,Dart 会自动创建 Future
对象。
如果函数不需要返回有效值,需要设置其返回类型为 Future<void>
。
11.3 处理返回结果
当异步函数最终返回结果之后,可以使用 Future
的 then
和 catchError
等函数来处理结果:
var future = lookUpVersion();
future.then((value) => print(value))
.catchError((error) => print(error));