Skip to content

认识Dart

Dart 语言的最初定位时一种运行在浏览器中的脚本语言。
因为使用 JavaScript 开发的程序混乱不堪,没有严谨的程序范式与语言数据类型限定,所以 Dart 就是为了解决 JavaScript 存在的在语言本质上无法改进的缺陷而设计的。

最初,Google 自家的 Chrome 浏览器中内置了 Dart 虚拟机(Dart VM),可以直接高效地运行 Dart 代码。
在 2015 年前后,由于少有项目使用 Dart 语言,所以 Google 将 Dart VM 引擎从 Chrome 中移除。
再后来,Google 内部孵化了开发移动框架 Flutter,并且在 Google 的操作系统 Fuchsia 中,Dart 被指定为官方的开发语言,前端开发框架的 Angular 也在持续迭代对应 Dart 版本的 AngularDart

Dart 属于应用层编程语言,通常情况下运行在自己的虚拟机上,但是特定情况下,它也可以编译成本机代码在硬件上
(比如在移动开发框架中,Flutter 会将代码编译成指定平台的本机代码以提高性能)

编译模式概述

在程序开发中,编译模式一般分为 JIT 和 AOT 两大类

JIT 全称为 Just in Time(即时编译),如 V8 JS 引擎,它能够即时编译和运行 JavaScript 代码。
这种模式的优点就是可以直接将代码分发给用户,而不用考虑机器架构,缺点就是源代码量大,将会花费 JIT 编译器大量的时间和内存来编译和执行。

AOT 全称为 Ahead of Time(事前编译),典型的例子就是像 C/C++ 代码需要被编译成特殊的二进制文件,才可以通过进程加载和运行。
这种模式的优势就是速度快,在密集计算或者图形渲染的场景下能够获得比较好的用户体验。

在对 Flutter App 进行代码开发时(Debug 模式),使用热更新(Hot Reload) 可以方便快速地刷新 UI,同时也需要比较高的性能来进行视图渲染,所以 Flutter 在 Debug 模式下使用了 Kernel Snapshot 编译模式(Dart 的 bytecode 模式,不区分架构,Flutter 项目内也叫做 Core Snapshot,可以归类为 JIT 编译)

在生产阶段,应用需要非常快的速度,所以 Flutter 使用的是 AOT 编译模式

Dart 语言

Dart 属于强类型语言,其中 var、dynamic 用来声明动态类型变量,与 JavaScript、Kotlin、Java10 中的 var 等语言类似。
与 JavaScript 不同的是在 Dart 中 var 一旦指定类型后,后期是不能再次修改类型的,dynamic 关键字声明的数据类型与 JavaScript 中的一致。
先看一段 JavaScript 代码

1
2
3
4
// JavaScript 中声明
var flag = "张三";  // 字符串类型
//  重新赋值数字类型
flag = 33;  // 类型修改成数值类型   

在上述 JavaScript 代码中 var 声明的变量类型可在后期随赋值类型的修改而改变。
在 Dart 中关键字 dynamic 声明的类型与 JavaScript 中 var 声明的类型一致,代码描述如下:

1
2
3
// Dart 中声明
dynamic flag = "张三" // 字符串类型
flag = 33    

在 Dart 中,使用 var 是不允许像 JavaScript 中使用 var 一样修改关键字类型的

Dart 中 number 类型分为 int 和 double。Dart 中没有 float 类型

Dart 中的变量声明方式如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 全局变量定义  
// 以下划线开头的只可以在文本文件中调用  
int _number = 10

// 全局变量定义 项目工程中都可以引用   
Int versionCode = 100

class UserBean {
    // 只可以在本类中访问 外部如果需要访问,可以通过 get 方法使用
    int _age;  
    // 定义 set 方法 在类外部为 _age 属性赋值  
    void set age=>age;    
    // 定义 get 方法 在类外部获取 age 属性的值   
    int get age=>_age;     

    // 未使用下划线开头定义的斌阿亮可直接通过类对象实例引用
    String username;   
    // 静态变量可以直接使用类引用
    static String userSchool;   
}

如下测试函数中使用 UserBean 中的变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void testUserBean() {
    // 无需创建 UserBean 对象,直接使用类名引用
    String userSchool = UserBean.userSchool

    // 创建 UserBean 的实例对象
    UserBean userBean = new UserBean()
    // 或者可以这样来写
    UserBean userBean1 = UserBean()

    // 然后使用类实例来访问 username
    String username = userBean.Username
    // 使用类实例来访问 _age 属性 通过 get 方法 age
    int age = userBean.age  
}

基本数据类型

在 Dart 语言中,我们常用的基本数据类型主要包括数字、字符串、布尔、列表、集合以及映射

数字

在 C 语言中,对数字的描述可以分为整数型和浮点型。Dart 同样如此,其数字分为 int 和 double 两种类型,它们都是 num 的子类

Dart 语言中的 int 类型通常为 64 位有符号整型,double 类型为 64 位有符号浮点数类型,符合 IEEE 754 标准。
需要注意的是,当把 Dart 代码编译成 JavaScript 代码时,如果 JavaScript 中的 number 类型对精度有要求,则最大只能使用 54 位有符号整型数字。在有精度要求的情况下,int 类型的数字取值范围最好在 -2^532^53 - 1 之间

下面我们尝试声明两个数字,它们分别为 int 类型和 double 类型:

1
2
int firstInt = 1;
double firstDouble = 1.001;

当然,也可以直接通过十六进制或者小数的科学计数法对数字进行表示,分别如下:

1
2
int hexInt = 0x10ABCD;
double exponentDouble = 1.002e5;

字符串

可以说,字符串是日常使用中最重要的数据类型之一。在 Dart 中,我们使用单引号 (')、双引号 (") 来声明字符串类型的数据。下面举几个例子:

1
2
3
String firstString = 'This\'s is and string';
String secondString = "This's is another string";
String stringWithEmoji = "This string contains an emoji 😂"; // 最后一个字符为 emji 表情

除此之外,我们也可以利用加号 (+) 将多个字符串类型的数据拼接在一起。
如果我们拼接的目标里包含其他类型的对象,Dart 会先自动调用 toString 方法将此对象转换成字符串对象,再进行拼接。
如果需要在文件中嵌入多个变量或者表达式,那么直接使用 + 进行拼接并不是很方便,这时可以使用 ${} 语法。
如果 {} 内仅包含一个变量标识符,我们还可以省略 {}。下面我们尝试通过不同的方式声明和拼接字符串类型的数据:

1
2
3
4
String funny = 'Funny';
String concat = 'It is' + funny + 'Flutter';
String expression = 'It is ${funny.toLowerCase()} Flutter';
String identifier = 'It is $funny Flutter';

布尔

Dart 专门为布尔变量创建了一个布尔类型。这个类型只有两个对象实例 -- true 和 false。需要注意的是,这两个对象都是编译时常量。

在 C 语言的判断语句中,我们可以将非 0 指传递给 if 表达式,编译器会自动将其转换为布尔值进行逻辑判断,但这在 Dart 中是非法的。
在 Dart 中,类似 if 和 assert 这样的表达式,仅支持传入布尔值进行逻辑判断。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int intValue = 1;
assert(intValue); // 编译时错误

bool boolValue = true;
assert(boolValue); // true

int doubleValue = 1.0;
if (doubleValue == 1.0) {
    print('equal'); // 输出 equal
}

列表

1
2
3
4
5
List<String> list = ['f', 'l', 'u', 't', 'e', 'r'];
assert(list[0] == 'f'); // 结果为 true

list[0] = 'm';
assert(list[0] == 'm'); // 结果为 true

集合

相对于列表,集合中的元素不重复且无序,不支持随机访问,具有更好的查找性能。在 Dart 中,可以使用 {} 来声明一个集合。
当声明的集合中包含内容时,编译器会自动对这个内容进行类型推导。
当声明的集合为空时,编译器就无法自动推导类型了,因此建议在声明集合时使用泛型显式声明内容类型。
如果使用 var 关键字来声明一个空集合,由于声明映射时已经占用了 {},因此要显式地写明泛型,例如 <String>{},否则编译器会报错。
下面声明两个集合对象,并做一些简单的测试:

1
2
3
4
5
6
7
Set<String> set = {'dart', 'flutter'}
assert(set.length == 2); // 结果为 true

var happiness = <String>{}; // 或 Set<String> happiness = {}     
happiness.add('funny');  
happiness.addAll(set);
assert(happiness.lenght == 3); // 结果为 true

映射

在日常的开发工作中,有许多场景离不开映射,例如将英文单词映射成中文,将 ID 映射成用户名等。
我们可以对任意两个 Dart 对象进行映射,需要注意一个键名只能有一个与之对应的键值。
在 Dart 代码中声明映射对象时,可以使用跟声明集合对象时相同的符号 -- {}。区别在与在映射中使用时,要用 : 符号分隔键名和键值。
声明好后,Dart 编译器会自动进行类型推导。需要注意的是,如果使用 var 关键字来声明一个空映射,那么默认会把 {} 推导为映射类型,如果想用 {} 声明集合,则需要显式地写明泛型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Map<String, String> map = {
    'dictonary': '字典',
    'map': '映射',
};
assert(map.length == 2); // true

var nameBook = {};
assert(nameBook.length == 0); // true
nameBook[1] = 'Jony Wang';
nameBook[66] = 'Linda Zhang';
assert(nameBook[66] == 'Linda Zhang'); // true
assert(nameBook[2] == null); // true

Map,List,Set的基本使用

Map 用来存储对象类型的数据,List 与 Set 用来存储数组类型的数据

Map 用来保存 key-value(键值对)的数据集合,分为 HashMap(无序)、LinkedHashMap(有序)、SplayTreeMap(查询快)。
Map 的创建实例如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 创建一个 Map 实例,默认实现是 LinkedHashMap   
Map()

// 创建一个 LinkedHashMap 实例,包含 other 中的所有键值对  
Map.from(Map other)

// 创建一个 Map 实例,其中 key 和 value 由 iterable 的元素计算得到
Map.fromIterable(Iterable iterable, {K key(element), V value(element)})

// 将指定的 keys 和 values 关联,创建一个 Map 实例
Map.fromIterables(Iterable<K> keys, Iterable<V> values)

// 使用 LinkedHashMap 创建一个严格的 Map 
Map.identity()

// 创建一个不可修改、基于哈希值的 Map,包括 other 的所有项     
Map.unmodifiable(Map other)

在实际项目中结合数据创建 Map 实例,创建一个空 Map 的代码如下

1
2
3
4
5
6
// 创建一个 Map 实例,按插入顺序进行排序,默认无数据
var dic = new Map()
print(dic)  // {}
// 创建一个空的 Map,Map 允许 null 作为 key
var dic5 = new Map.identity()
print(dic5);  // {}

创建有一个有初始值的 Map,代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 根据一个 Map 创建一个新的 Map,按插入顺序进行排列
var dic1 = new Map.from({'name': '张三'})
print(dic1)  // {name: 张三}

// 根据 List 创建 Map,按插入顺序进行排列
List<int> list = [1, 2, 3]
// 使用默认方式,key 和 value 都是数组对应的元素
var dic2 = new Map.fromIterable(list)
print(dic2) // {1: 1, 2: 2, 3: 3}

// 设置 key 和 value 的值
var dic3 = new Map.fromIterable(list, key: (item) => item.toString(), value: (item) => item * item)
print(dic3) // {1: 1, 2: 4, 3: 9}

// 创建一个不可修改,基于哈希值的 Map
var dic6 = new Map.unmodifiable({'name': 张三})
print(dic6)  // {name: 张三}

根据 List 数据来创建 Map,代码如下

1
2
3
4
5
6
// 两个数组映射一个字典,按插入顺序进行排序  
List<String> keys = ['name', 'age']
var values = [张三, 20]
// 如果有相同的 key 值,后面的值会覆盖前面的值
var dic4 = new Map.fromIterables(keys, values)
print(dic4)  // {name: 张三, age: 20}

对于 Map 来讲,初始化创建时可以赋值也可以是空的。
在实际开发中,当创建可变的 Map 数据集合时,往往会根据不同的操作来修改不同的数据,代码如下

1
2
3
4
5
6
7
8
9
// 根据一个 Map 创建一个新的 Map,按插入顺序进行排序  
// 在这里通过泛型指定了 Map 中 key 的类型为 String 类型,value 是动态的   
Map<String, dynamic> dic1 = new Map.from({'name': '张三'})
print(dic1)  // {name: 张三}

// 修改 name 的值
dic1['name'] = '李四'
// 向 Map 中添加新的键值对数据
dic1['age'] = 23

然后获取 Map 中的数据,操作如下

1
2
3
4
5
6
7
// 根据 key 获取对应的值
String name = dic1['name']

// 遍历获取 Map 中所有的数据
dic1.froEach((key, value) {
    print("${key} is ${value}")
})

List 与 Set 都是用来存储数组类型数据,区别是 Set 不可保存重复数据,也就是说 Set 中的数据具有唯一性。
在这里只分析 List,Set 与 List 的使用方法一致,代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 创建非固定长度的 List
var testList = List()
// 也可以 List testList = List()
print(testList.length) // 0
// 创建固定长度的 List
var fixedList = List(4)
print(testList.length) // 4

// 向 List 中添加数据
testList.add("hello")
testList.add(123)

// 创建元素类型固定的 List 
var typeList = List<String>() // 只能添加字符串类型的元素
typeList.add("张三") // 正确
typeList.add(1)  // 错误 类型不正确

// 直接赋值 创建 List
var numList = [1, 2, 3]

获取 List 中数据的方法也比较多,如下是获取 List 中单个元素值(获取 List 中指定位置的值)的基本方法

1
2
3
4
// 直接根据索引获取 0 号位置上的数据
String value = list[0]
// 等效于 elementAt 方法获取
String value1 = list.elementAt(0)

查找 List 中的元素

1
2
3
4
5
6
7
List<String> list = ["test1", "xioming", "张三", "xioming", "张三", "李四"]
// 从索引 0 处开始查找指定元素,返回指定元素的索引
int index = list.indexOf("张三")  // index 2
// 从索引 0 处开始查找指定元素,如果存在则返回元素索引,否则返回 -1
int index2 = list.indexOf("张三", 3) // 4
// 从后往前查找,返回查找到的第一个元素的索引
int index4 = list.lastIndexOf("张三") // 4   

循环遍历 List 中的数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建测试用的 List 
List<String> testList = ["test1", "xioming", "张三", "xioming", "张三", "李四"]   
// 方式一 遍历获取 List 中的所有数据
testList.froEach((value) {
    // value 就是 List 中对应的值
})
// 方式二 遍历获取 List 中的所有数据
for (int i=0; i < testList.length; i++) {
    // 根据索引获取 List 中的数据
    var value = testList[i]
}
// 方式三
// while + iterator 迭代器遍历,类似 Java 中的 iterator
while (testList.iterator.moveNext()) {
    // 获取对应的值
    var value = testList.iterator.current
}
// 方式四 增强 for 循环
// for-in 遍历
for (var value in testList) {
    // value 就是 List 中对应的值
}

List 数据结构转 Map 数据结构

1
2
3
4
5
List<String> testList = ["test1", "xioming", "张三", "xioming", "张三", "李四"]
print(test)  // [test1, xioming, 张三, xioming, 张三, 李四]
// 将 list 转为 Map 结构数据
Map<int, String> map = testlist.asMap()
print(map);  // {0: test1, 1: xioming, 2: 张三, 3: xioming, 4: 张三, 5: 李四}

随机排列 List 中的数据顺序

1
2
3
List<String> testList = ["test1", "xioming", "张三", "xioming", "张三", "李四"]
testList.shuffle()
print(testList) // [test1, xioming, xioming, 李四, 张三, 张三]

升序排列 List 中的数据:

1
2
3
List<String> testList = ["A", "D", "F", "F", "B", "C"]
testList.sort()
print(testList)  // [A, B, C, D, F, F]

合并 List 中的数据:

1
2
3
4
5
6
7
List<int> list = [1, 2, 3, 4]
List<int> list2 = [2, 3, 4, 5]

Iterable<int> list3 = list2.followedBy(list)
print("list: "+list.toString())  // list: [1, 2, 3, 4]
print("list2: "+list.toString()) // list2: [2, 3, 4, 5]
print("list3: "+list.toString()) // list3: [2, 3, 4, 5, 1, 2, 3, 4]

方法函数

在 Dart 语言中定义方法的方式与其他语言类似,方法定义格式如下:

1
2
3
返回值类型 方法名称(参数) {
    return 返回值
}

在 Java 中,方法名称一样但是参数不一样,被称为方法的重载,在 Java 中写法如下

1
2
3
4
5
6
void test1() {

}
void test1(String name) {

}

在 Dart 中定义方法的重载,一个方法就可达到效果,代码如下

1
2
3
void test1([String name]) {

}

上述声明的 test1 方法在使用时直接调用 test1() 或者 test1("张三"),还可使用 Objective-C 的方式来声明变量名称以调用 test1(name: "张三"),这种以变量名称显式的调用方法定义代码如下:

1
2
3
void test1({String name}) {

}

在 Dart 中也有剪头函数,上述的 test1 方法简写成箭头函数代码如下:

1
void test1({String name}) => print("arrow function")

声明

几乎所有的编程语言中都有函数相关的概念,我们可以简单地把它理解为一个包含输入和输出的代码集合。
在 Dart 中,函数和其他数据类型一样是一等公民,意味着我们可以像传递值一样传递函数。
Dart 中所有的函数都是 Function 的实例,这也是可以将函数作为值和参数来传递的原因之一。函数既能以 fun(){} 的方式声明,也能用 => 声明。
函数返回值的类型是可以省略的,如果省略了返回值的类型,那么编译器会自动根据返回值做类型推导。
不过,Dart 官方不推荐这样的做法。简单的示例代码如下:

1
2
3
4
5
6
7
8
9
String funny() {
    return "funny flutter"
}

lonelyFunny() {
    return "lonely funny flutter";
}

String shortFunny() => "short funny flutter";

需要注意的是,使用 => 声明函数是类似于 { return expression; } 的简化方式。

参数

在上面的示例中,我们声明的都是不带参数的函数,下面我们一起学习带参数的函数。和其他语言类似,Dart 中函数的参数也分为必选参数和可选参数。
其中必选参数可以省略,即采取默认值,如果不省略就必须放在前面,可选参数则放在靠后的位置。
根据是否命名,参数可以分为位置参数和命名参数,下面我们介绍一下各类参数的声明和使用方式

(1) 必选位置参数
跟 C 语言和 Java 语言类似,Dart 支持使用位置来区分参数。在调用函数时,需要完全按照声明函数时的参数顺序传入各个参数。下面是一个示例:

1
2
3
4
5
void sayHello(String name, String greeting) {
    print("Hello $name, $greeting.");
}

sayHello("Jony", "nice to meet you");  // 输出 Hello Jony, nice to meet you.    

这里的 name 和 greeting 都是必选参数,如果没有传入对应的参数,那么 Dart 编译器会报错。

(2) 可选位置参数

可选位置参数需要使用 [] 声明。如果有必选位置参数,那么需要放在可选位置参数的前面。示例声明如下:

1
void positionBaseOptionalParamter(String a, [String b, String c])

调用这个函数时,要按顺序传入各个参数。如果没有给可选位置参数传值,那么 Dart 会默认传入 null 作为这个可选参数的值。示例调用如下:

1
positionBaseOptionalParamter("test", "b String"); // 参数 c 为 null

(3) 命名参数
命名参数需要使用 {} 声明。虽然这个声明形式跟声明可选位置参数时差不多,但在调用的时候,需要以“参数名:参数值”的方式传入参数。
还是用 sayHello 函数作为示例:

1
2
3
4
5
void sayHello({@required String name, @required String greeting}) {
    print("Hello $name, $greetnig.");
}

sayHello(name: "Jony", greetings: "nice to meet you"); // 输出 Hello Jony, nice to meet you.

我们可以看到这里出现了一个新的关键字 @required,这是 Flutter 框架提供的一个注解,能帮助我们标识哪里命名参数是必须传入的

闭包

在 Dart 中,我们可以像声明变量一样在任意地方声明函数。
如果我们在一个函数的内部声明另一个函数,那这个内部函数是可以访问其函数外部的变量的,外部的变量将会自动被内部函数捕获,形成闭包。
所以,我们可以简单地把闭包理解成函数与上下文捕获机制的结合。下面,我们来举一个函数闭包的例子:

1
2
3
4
5
6
7
8
void outterFunction() {
    String hello = 'hello';

    void innerFunction() {
        String world = 'innerFunction';
        print('$hello $world'); // 输出 "hello world"
    }
}

这里我们首先在 outterFunction 的函数体中声明了一个 innerFunction 函数。在 innerFunction 函数中,可以访问和调用 outterFunction 函数中的变量

main 函数

和 C 语言一样,Dart 中也有一个名为 main 的入口函数,程序会以 main 函数为入口开始执行。
Dart 中的 main 函数没有返回值,void 关键字可以省略,声明成 main 或者 void main 都可以。下面我们举一个实际的例子:

1
2
3
void main() { // 或 main()
    print('Hello world.');
}

另外可以声明 List<String> 来接收外部传入的参数,这通常在编写命名行工具时使用,用于获取控制台传入的参数。
下面我们来编写一个带有参数的 main 函数:

1
2
3
void main(List<String> args) {
    print('Hello $args');
}

匿名函数

大部分函数是有名字的,我们把没有名字的函数称为匿名函数。在 Dart 中,函数名字并不是必须的,如果有需要那么名字可以省略。
例如,在遍历容器时,我们希望传入一个函数对容器内的每个成员都执行一遍这个函数定义的操作,这时函数的命名就显得不那么重要。举一个例子:

1
2
3
4
List<String> words = ['funny', 'flutter'];
words.forEach((word) {
    print('$word');
});

就像这个例子,如果匿名函数内只有一行代码,那么在声明该函数时还可以省略 {},直接使用 => 声明返回值。代码如下:

1
2
List<String> words = ['funny', 'flutter'];
words.forEach((word) => print('$word') ); // 输出 funny\nflutter

流程控制

利用 if 来判断

if-else 逻辑判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void main() {
    var numbers = [1, 2, 3, 4, 5, 6, 7]
    for (var i = 0; i < numbers.length; i++) {
        if (numbers[i].isEven) {
            print('偶数: ${numbers[i]}')
        } else if (numbers[i].isOdd) {
            print('奇数: ${numbers[i]}')
        } else {
            print('非法数字')
        }
    }
}
1
2
3
4
5
6
7
if (isRaining()) {
    you.bringRainCoat();
} else if (isSnowing()) {
    you.wearJacket();
} else {
    car.putTopDown();
}

三元运算符 ?:

1
2
3
4
5
6
7
void main() {
    var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    for (var i = 0; i < numbers.length; i++) {
        num targetNumber = numbers[i].isEven ? numbers[i] * 2: numbers[i] + 4
        print(targetNumber)
    }
}

利用 for/while 来循环

for 循环

1
2
3
4
5
6
main() {
    List<String> colorList = ['deep', 'black', 'blue', 'green']
    for (var i = 0; i < colorList.length; i++) { // 可以用 var 或 int
        print(colorList[i])
    }
}
1
2
3
4
5
6
7
8
for (int i = 0; i < 5; ++i) {
    print('$i ')
} // 0 1 2 3 4

List<int> array = [1, 2, 3, 4, 5]
for (int item in array) {
    print('$item ')
} // 1 2 3 4 5

while 循环

1
2
3
4
5
6
7
main() {
    List<String> colorList = ['deep', 'black', 'blue', 'green']
    var index = 0
    while (index < colorList.length) {
        print(colorList[index++])
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int count = 1;
while (count > 0) {
    print('$count ');
    --count;
} // 输出 1

int index = 0;
do {
    print('$index ');
    --index;
} while (index > 0);

do-while 循环

1
2
3
4
5
6
7
main() {
    List<String> colorList = ['deep', 'black', 'blue', 'green']
    var index = 0
    do {
        print(colorList[index++])
    } while (index < colorList.length)
}

使用 break 和 continue 可以分别结束整个循环和跳过当此循环。以下示例展示了 break 和 continue 的作用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
List<int> array = [1, 2, 3, 4, 5];
for (int item in array) {
    if (item == 1) {
        continue;
    }
    if (item == 4) {
        break;
    }
    print('$item ')
} // 输出 2 3

利用 switch 来选择

相较 C 语言,Dart 对 switch 语句做了加强。Dart 中的 switch 不仅可以比较整数,还可以比较字符串以及其他编译期常量。
需要注意的是,利用 switch 进行比较的变量必须和 case 语句中的变量类型相同,并且不能重写比较函数 ==。
除了空 case 语句,其他 case 语句必须以 break、continue、throw 或者 return 为结尾,否则编译器将抛出错误,其中 continue 需要和 label 结合使用。
如果所有 case 语句都未命中,switch 将自动执行 default 语句。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
switch (state) {
    case 'RETRY':
        continue label;
label:
    case 'CLOSED':
    case 'NOW_CLOSED':
        executeNowClosed();
        break;
    default:
        throw UnsupportedStateException('Unsupported state: $state')
}

switch-case 选择语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Color getColor(String colorName) {
    Color currentColor = Colors.blue
    switch (colorName) {
        case "red":
            currentColor = Colors.red
            break
        case "blue":
            currentColor = Colors.blue
            break
        case "yellow":
            currentColor = Colors.yellow
            break
    }
    return currentColor
}

异常处理

在计算机科学中,异常是一个极其重要的概念。当底层遇到无法处理的问题时,就会向上层抛出异常,由上层决定程序接下来的状态。
在操作系统中,也可以用信号的形式抛出异常。有些异常可以被程序接管处理,但也有些异常(如内存溢出异常)可能导致整个程序直接被终止执行

抛出异常

Dart 提供了 Exception 和 Error 两种形式的基础异常类型。
一般来说,Dart 建议上层的用户代码应该保护抛出的 Exception,而不应该保护抛出的 Error,因为 Error 表示一个运行时的错误。
例如当 List 对象为空时,List.first 方法会抛出 StateError 以表示错误。
当然,Dart 也可以将任意非空对象当作异常抛出,但这并不是 Dart 官方建议的做法。示例代码如下:

1
2
throw Exception('This is first dart exception');
throw 'Network connection closed';

捕获异常

底层抛出异常后,上层会立即停止执行异常后面的代码,转而去执行异常控制程序。在 Dart 中,底层的异常能够被上层捕获和处理。
不仅如此,Dart 还支持捕获和处理特定类型的异常。未被捕获的异常则继续向上层传递,直到没有任何代码捕获它,如果一直没有代码捕获并处理这个异常,那么 Dart 会执行默认的异常处理逻辑,退出有异常的程序。示例代码如下:

1
2
3
4
5
6
7
8
9
try {
    ... // 其他代码
    throw FormatException('Format is wrong');
} on FormatException {
    print('Do not worry about it');
} catch(e) {
    print('Other exception $e');
    rethrow;
}

除了异常对象之外,Dart 还提供了一场堆栈参数以便用户排查问题。
对于某些无法处理的异常,Dart 允许使用 rethrow 语句将此异常重新抛给上层,由上层处理它。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try {
    someExceptionalFunction();
} on Exception catch(e) {
    print('Exception catched $e');
} on FileNotExsitException catch(e) {
    print('Record exception $e');
    rethrow;
} catch(e, s) {
    print('Unexpected exception $e catched, call stack: $s');
}

使用 finall 保证代码一定被执行

前面我们提到,一旦遇到异常,程序将立即跳转到异常控制程序,而不执行异常后面的代码。
但有时我们需要在处理完异常后,执行一些代码逻辑(如清理工作),此时就需要用到 finally 代码块。
finally 代码块紧跟在 try catch 代码块后面,等异常处理结束后,Dart 能够保证 finally 代码块中的逻辑得到执行。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
try {
    someExceptionalFunction();
} finally {
    doSomeCleanUp();
}

try {
    someExceptionalFunction();
} catch(e) {
    print('Exception catched $e');
} finally {
    doSomeCleanUp();
}

Dart 是一种面向对象的语言,其每一个对象都是一个类的实例。
类是面向对象设计程序时实现信息封装的基础,我们可以将一些数据和方法封装在其中,对外暴露接口,屏蔽内部的具体实现。
相对于 C++ 这种面向对象的语言,Dart 采用单继承的机制,即一个类只能有一个父类。
如果想让一个类继承多个父类,可以使用 mixin(混入) 机制。mixin 和 Swift 中的 Extension 类似,可以往一个类中混入其他类已实现的一些方法,而不需要继承其他类

类的成员变量

一个类往往拥有很多成员变量和方法,在实例化一个类对象时,Dart 会为此申请内存空间以保存其成员变量和方法。
任何一个 Dart 对象都是一个类的实例,利用这个对象可以访问类的成员变量和方法。下面就实例化一个 Point 类对象,请注意这里并未定义 Point 类

1
2
3
4
5
6
7
// 示例代码,未定义 Point 类

Point p = Point(2, 2);
p.y = 3;
assert(p.y == 3);

num distance = p.distanceTo(Point(4, 4));  

注意,对一个 null 对象使用 . 语法会导致 Dart 程序抛出异常,因此在使用 . 语法时应该确保变量值非 null。
我们也可以使用 ?. 来保证不访问取值为 null 的变量或者方法,达到省去判空操作的目的。
?. 的具体语义是当前对象如果不为空,就返回对应的成员变量;如果为空,则返回 null。示例代码如下:

1
2
3
4
5
6
7
8
9
var p = null

p.x; // 抛出异常

if (p != null) {
    print(p.x); // 若 p 非空,则打印 p.x 的值
}

print(p?.x); // 若 p 为空,则打印 null

类的构造方法

一般而言,要使用一个类的成员变量,先要创建一个类对象。
在面向对象的语言中,能够创建对象实例的方法被称为构造方法,我们可以给构造方法传入具体的参数来构造一个特定的对象。
在 Dart 中,构造方法只能跟类名相同,或者是一个类方法。这里会定义一个 Point,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Point {
    num x = -1;
    num y = -1;

    Point(num x, num y) {
        this.x = x;
        this.y = y;
    }
    // 或者直接用 Point(this.x, this.y) 声明,与上面的函数等同

    Point.origin() {
        this.x = 0;
        this.y = 0;
    }
}

Point p1 = Point(1, 2);
Point p2 = Point.origin();

对于一些在实例中数据不会改变的类,Dart 可以利用常量构造方法构造一个常量对象,编译器会在编译期构造此对象。
需要注意的是,只有所有成员变量都被标注为 final 的类才可以使用常量构造方法。
使用 const 关键字可以声明常量对象,const 关键字可以在等号的左边或者右边,也可以同时出现。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class ImmutablePoint {
    final num x, y;
    const ImmutablePoint(this.x, this.y);
}

ImmutablePoint originPoint = const ImmutablePoint(0, 0);
const ImmutablePoint otherPoint = ImmutablePoint(0, 0);
const ImmutablePoint doubleConstPoint = const ImmutablePoint(0, 0);

// identical 函数用于检查两个变量是否引用同一个对象
assert(identical(originPoint, otherPoint)); // true
assert(identical(originPoint, doubleCoonstPoint));  // true  

使用 getter 和 setter

我们通常不会直接利用对象访问类的成员变量。更常见的做法是利用 getter 方法和 setter 方法来访问和设置成员变量。
在 Dart 中,编译器会为我们自动生成 getter 方法和 setter 方法,但有时需要我们自行实现,例如需要对传入的值进行计算时。
对于这种情况,Dart 提供了 get 关键字和 set 关键字。
自定义实现 getter 方法和 setter 方法之后,调用方并不需要更改原来的调用方式。以下示例展示了 get 关键字和 set 关键字的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Rectangle {
    num left, top, width, height;

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

    num get right => left + width;
    set right(num value) => left = value - width;
}

Rectangle rectangle = Rectangle(1, 2, 3, 4);
rectangle.right = 1; // setter
print(rectangle.right); // getter

继承

继承是面向对象设计中的一个基本概念,可以使子类具有父类的属性和方法。我们也可以给子类增加属性,重新实现和追加实现一些方法等。

有了继承机制,我们可以更好地利用面向对象的设计思路实现抽象。
例如动物 (Animal) 类中已经实现了一个走路 (walk) 方法,当一个子类继承该类时,我们可以在子类的走路方法中实现特定的功能。
在下面的代码中,人 (Human) 类和猫 (Cat) 类都继承自动物父类,我们分别在两个类中对走路姿态做了额外定义:

 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
class Animal {
    void walk() {
        print('animal is getting away from here');
    }
}

class Cat extends Animal {
    @override
    void walk() {
        super.walk();
        print('cat is wagging tail');
    }
}

class Human extends Animal {
    @override
    void walk() {
        super.walk();
        print('human is waving hand');
    }
}

Cat cat = Cat();
Human human = Human();
cat.walk(); // 输出 animal is getting away from here\ncat is wagging tail
human.walk(); // 输出 animal is getting away from here\nhuman is waving hand

在实际的工程实践中,为了很好地区分开重写方法和其他方法,一般会在重写方法前加上 @override 注解。

抽象机制与抽象类

利用继承机制可以重新实现父类的方法,如果一个类想预留一些没有实现的方法给子类实现,那么可以使用抽象机制。
在 Dart 中,实例方法、setter 方法和 getter 方法都可以是抽象的。要想在一个类中使用抽象方法,必须先利用 abstract 关键字声明此类为抽象类。
抽象类中含有未被实现的抽象方法,因此不能被直接实例化。
抽象类中也可以包含部分方法的实现,当某个子类继承抽象类时,它需要先重写抽象类中的所有抽象方法,之后子类才可以被实例化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
abstract class Animal {
    void play(); // 定义一个没有实现的抽象方法 play
}

class Human extends Animal {
    @override
    void play() {
        print('human playing video game');
    }
}

Human human = Human();
human.play(); // 打印 human playing video game   

隐式接口

Dart 没有为借口提供一个专用的关键字,但是在 Dart 的定义中,每个类都是一个隐式的接口。
利用 implements 关键字,一个类可以实现另一个类的所有实例变量和实例方法。如下实例代码中的 Impostor 类就实现了 Person 类中的 _name 变量和 greet 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Person {
    final _name;
    Person(this._name);
    String greet(String who) => 'Hello, $who. I am $_name.';
}

class Impostor implements Person {
    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()));
}

继承之外的另一种选择: mixin

Dart 和大多数面向对象的语言一样,也是采用的单继承机制。也就是说,一个类只能有一个父类。
但很多时候,我么需要的是一种能力的组合而非简单的继承,借助 mixin 机制可以轻松实现这个功能。
当然,我们也可以抽象出层级结构更多的父类以覆盖所有能力,但这并不十分灵活。

使用 mixin 关键字可以声明一个 mixin 实例,使用 with 关键字可以将 mixin 实例赋值给一个特定的类。
下面我们举个例子,来感受一下 mixin 机制和 mixin、with 关键字的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
mixin Driver {
    driverLicence = true

    void driveCar() {
        print('I can driving')
    }
}

mixin Cooker {
    void makeFood() {
        print('I am cooking')
    }
}

class Person with Driver, Cooker {

}
Person a = new Person()
a.driveCar(); // 输出 I can driving

在上面的示例中,我们通过把实例 Driver 和 Cooker 赋值给 Person 类,让人具有开车和做饭的能力。
简单来讲,mixin 机制就是将特性或者能力赋予某个类。在 Dart 中,即使一个类没有使用 mixin 关键字声明,使用 with 关键字也可以把它作为 mixin 实例赋值给另一个类。
示例代码如下:

1
2
3
4
5
6
7
8
9
class Listenable {
    void listen() {
        print('I am listening')
    }
}

class Channel with Listenable {

}

我们也可以使用 on 关键字限定某个 mixin 实例只能由特定的类使用。
在这样的 mixin 实例中,可以调用原类中的函数。下面的示例就限定了实例 Flyable 只能由 Bird 类使用,并在 Flyable 中调用了 Bird 类的函数 saying:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Bird() {
    void saying() {
        print('tweet tweet tweet')
    }
}

mixin Flyable on Bird {
    void fly() {
        print('I am flying');
        saying();
    }
}