Flutter之一Dart语言编程基础
一.Dart 简述
在Dart中,一切都是对象,每个对象都是一个类的实例,所有对象都继承自Object。Dart是强类型的,也有自动推断类型的机制。
二.变量。
1.变量的声明。
Dart中定义变量有两种方式,一种是静态类型语言常用的方式,显式指定变量类型,另一种则是动态语言的常用方式,不指定类型,由vm自动推断。
变量的默认值:未初始化的变量的初始值为null。甚至具有数字类型的变量最初也是null,因为在Dart中没有基础的数据类型,所有的都是对象类型。
2. 显式指定类型来定义变量
1 | void main() { |
3. 隐式推导变量.
无需指定变量的数据类型,变量会根据值的类型进行推导,使用关键字var。
1 | void main() { |
4 动态数据类型.
如想动态改变变量的数据类型,应当使用dynamic或Object来定义变量。
1 | void main() { |
dynamic:
在 Dart 中,所有值都是对象,但对象的类型系统有层级之分。dynamic 是一种特殊的“顶层类型”,和 Object 类似,在大多数情况下,不直接使用它 通过它定义的变量会关闭类型检查,这意味着 dynamic x= 'hal'; x.foo();这段静态类型检查不会报错,
但是运行时会crash,因为x 并没有foo() 方法,因为dynamic 会让 Dart 放弃类型检查,带来运行时风险;除非在特殊场景(如 JSON 解析、反射、第三方库交互)确实需要动态类型,否则:不推荐直接使用 dynamic定义变量。
object:
是Dart 对象的基类,当你定义: object o =xxx ;时这个时候系统会认为o 是个对象,你可以调用o的toString()和hashCode()方法,因为Object 提供了这些方法,但是如果你尝试调用o.foo()时,
因为o 并没有foo() 方法,静态类型检查会运行报错。
综上不难看出dynamic 与object 的最大的区别是在静态类型检查上。dynamic在编译期不检测,object在编译器则会检测。
建议在编写代码时,尽可能显式指定变量类型,这样可以提升代码可读性与调试的便利性。
4.变量的命名规则:
- 变量名称必须由数字、字母、下划线和美元符($)组成。
- 注意:标识符开头不能是数字。
- 标识符不能是保留字和关键字。
- 变量的名字是区分大小写的如: age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量。
- 标识符(变量名称)一定要见名思意 :变量名称建议用名词,方法名称建议用动词。
常用数据类型。
| 类型类别 | 类型 | 描述 | 示例 |
|---|---|---|---|
| 数字类型 | int |
整数,64位 | var a = 10; |
double |
浮点数,64位 | var b = 3.14; |
|
num |
int 和 double 的父类,可存放整数或浮点数 | num n = 5; n = 3.14; |
|
| 字符串类型 | String |
字符串 | var s = "hello"; |
| 布尔类型 | bool |
逻辑值,只有 true 和 false |
var flag = true; |
| 集合类型 | List<T> |
有序集合(数组),可变或不可变 | var list = [1,2,3]; |
Set<T> |
无序集合,不允许重复 | var set = {1,2,3}; |
|
Map<K,V> |
键值对集合(关联数组) | var map = {'name':'Tom','age':20}; |
|
| 动态类型 | dynamic |
可以存放任意类型,运行时可修改类型,编译器关闭类型检查 | dynamic x = 10; x = "hello"; |
Object |
所有对象的基类,类型安全,但可存放任意类型 | Object o = 10; o = "hi"; |
|
| 常量类型 | final |
运行时只读变量,只能赋值一次 | final name = "Tom"; |
const |
编译时常量,声明时必须初始化,值不可改变 | const pi = 3.14; |
|
| 特殊类型 | void |
表示无返回值 | void func() {} |
Null |
空类型,只有 null 值 |
Null n = null; |
|
Never |
永远不会有值,用于函数抛异常或死循环 | Never f() { throw Exception(); } |
1.数值类型
不像Java把类型分的特别细,比如整数类型,就有byte、short、int 、long 。Dart 内置支持两种数值类型,分别是int 和double ,它们的大小都是64位(具体取决于平台)。
数值类型转换:
1 | void main() { |
| 转换方向 | 方法 | 示例 |
|---|---|---|
String → int |
int.parse(String) |
int.parse('42') |
String → double |
double.parse(String) |
double.parse('3.14') |
int → String |
toString() |
123.toString() |
double → String |
toString() / toStringAsFixed(n) |
3.14159.toStringAsFixed(2) |
int ↔ double |
toDouble() / toInt() |
10.toDouble() / 3.9.toInt() |
2.字符串类型
Dart可以使用单引号或双引号来创建字符串
1 | var str1 = "hello"; |
Dart可以使用三引号来创建包含多行的字符串
1 | var multiLine1 = """xxxxxxx |
Dart支持使用”+”操作符拼接字符串
1 | var hw = "hello" + " world"; |
Dart提供了插值表达式”${}”,也可以用于拼接字符串
1 | var name = "张三"; |
与kotlin类似,Dart使用”==”来比较字符串的内容
1 | print("hello" == "world"); |
其他字符串操作功能
1 | void main() { |
| 分类 | 方法 / 属性 | 示例 | 说明 | 示例输出 |
|---|---|---|---|---|
| 基本信息 | length |
"hello".length |
获取字符串长度 | 5 |
isEmpty |
"".isEmpty |
判断是否为空字符串 | true |
|
isNotEmpty |
"abc".isNotEmpty |
判断是否非空 | true |
|
| 截取 | substring(start, end) |
"hello".substring(0, 2) |
截取子串(含头不含尾) | "he" |
substring(start) |
"hello".substring(3) |
从索引开始至末尾 | "lo" |
|
| 分割 | split(pattern) |
"a,b,c".split(",") |
按分隔符拆分为列表 | ["a", "b", "c"] |
| 查找 | startsWith(pattern, [index]) |
"hello".startsWith("he") |
是否以指定字符串开头 | true |
"hello".startsWith("lo", 3) |
从索引 3 开始判断开头 | true |
||
endsWith(pattern) |
"hello".endsWith("lo") |
是否以指定字符串结尾 | true |
|
contains(pattern) |
"hello".contains("el") |
是否包含子串 | true |
|
| 替换 | replaceAll(from, to) |
"a,b,c".replaceAll(",", "-") |
替换所有匹配内容 | "a-b-c" |
replaceFirst(from, to) |
"a,b,c".replaceFirst(",", "-") |
替换首个匹配内容 | "a-b,c" |
|
| 去空格 | trim() |
" hi ".trim() |
去除前后空格 | "hi" |
trimLeft() |
" hi".trimLeft() |
去除左侧空格 | "hi" |
|
trimRight() |
"hi ".trimRight() |
去除右侧空格 | "hi" |
|
| 大小写 | toUpperCase() |
"hi".toUpperCase() |
转大写 | "HI" |
toLowerCase() |
"Hi".toLowerCase() |
转小写 | "hi" |
|
| 拼接 | + |
"hello" + " world" |
字符串拼接 | "hello world" |
${} |
"name: ${userName}" |
模板字符串 | 如 "name: Tom" |
4.布尔类型
Dart中的布尔类型用法同Java,仅有false、true两个值,不能使用0、非0或者null、非null来表达false和true。与Java不同的是,布尔类型的默认值为null。
1 | void main() { |
5.列表类型(List)
Dart 的 List 是有序集合,既支持固定长度,也支持动态可变长度,提供丰富的操作方法,推荐尽量指定类型
- List 是 有序集合,可存放多个对象(元素)。
- Dart 的 List 类似于 Java 的 ArrayList,底层是动态数组。
- 可以存放 同类型元素(推荐)或 任意类型(不推荐)。
- 支持 固定长度 和 可变长度 两种形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14void main() {
// 可变列表
var list = [1, 2, 3]; // 创建列表
print(list[0]); // 获取下标为0的元素 => 1
print(list.length); // 列表长度 => 3
list.add(5); // 添加元素
print(list); // [1, 2, 3, 5]
// 不可变列表(编译时常量)
var constantList = const [1, 2, 3]; // const 修饰列表
print(constantList); // [1, 2, 3]
// constantList[1] = 1; // ❌ 报错:无法修改 const 列表
}
常用操作
| 操作 | 方法 / 属性 | 示例 | 输出 |
|---|---|---|---|
| 长度 | length |
list.length |
3 |
| 添加元素 | add(element) |
list.add(4) |
[1,2,3,4] |
| 添加多个元素 | addAll([..]) |
list.addAll([5,6]) |
[1,2,3,4,5,6] |
| 删除元素 | remove(element) |
list.remove(2) |
[1,3,4,5,6] |
| 删除指定索引 | removeAt(index) |
list.removeAt(0) |
[3,4,5,6] |
| 查找索引 | indexOf(element) |
list.indexOf(4) |
1 |
| 判断是否包含 | contains(element) |
list.contains(5) |
true |
| 遍历 | for / forEach |
list.forEach((e) => print(e)); |
逐个输出元素 |
| 排序 | sort() |
list.sort() |
升序排序 |
| 翻转 | reversed |
list.reversed.toList() |
翻转列表 |
| 清空 | clear() |
list.clear() |
[] |
6.映射(Map)
Map 又称 关联数组,相当于 Java 的 HashMap。Map 键值对无序,键和值可以是任意类型(一般键为 String 或 int)。每个键对应一个值,键不可重复。
通过字面量创建Map
1 | void main() { |
使用Map类的构造函数创建对象
1 | void main() { |
获取Map的长度
1 | print(person.length); |
查找Map
1 | pirnt(person["name"]); |
常用操作
| 操作 | 方法 / 属性 | 示例 | 说明 |
|---|---|---|---|
| 获取长度 | length |
person.length |
Map 中键值对数量 |
| 查找值 | [key] |
person['name'] |
获取对应键的值,不存在返回 null |
| 添加/修改 | [key] = value |
person['city'] = '北京' |
添加新键值对或修改已有值 |
| 删除 | remove(key) |
person.remove('age') |
删除指定键值对 |
| 判断键是否存在 | containsKey(key) |
person.containsKey('name') |
返回 true/false |
| 判断值是否存在 | containsValue(value) |
person.containsValue('张三') |
返回 true/false |
| 获取所有键 | keys |
person.keys |
返回所有键的 Iterable |
| 获取所有值 | values |
person.values |
返回所有值的 Iterable |
| 遍历 Map | forEach((k,v)=>…) |
person.forEach((k,v)=>print('$k:$v')) |
遍历键值对 |
7.去重集合(Set)
Dart 的 Set 是无序、不可重复的集合,支持可变和不可变操作,适合存储需要去重的数据。
- Set 是 无序集合,不允许重复元素。
- 类似于 Java 的 HashSet。
- 支持泛型
,提高类型安全。 - 可变或不可变(使用 const 创建不可变集合)。
字面量创建
1 | void main() { |
使用 Set 构造函数
1 | void main() { |
常用操作
| 操作 | 方法 / 属性 | 示例 | 说明 |
|---|---|---|---|
| 添加元素 | add(value) |
set.add(4) |
添加元素,重复元素会被忽略 |
| 添加多个元素 | addAll([..]) |
set.addAll([5,6]) |
批量添加元素 |
| 删除元素 | remove(value) |
set.remove(2) |
删除指定元素 |
| 清空 | clear() |
set.clear() |
删除所有元素 |
| 判断是否包含元素 | contains(value) |
set.contains(1) |
true/false |
| 获取长度 | length |
set.length |
集合中元素数量 |
| 遍历 | forEach((e) => ...) |
set.forEach(print) |
遍历元素 |
| 转 List | toList() |
set.toList() |
将 Set 转为 List |
| 转 Set | toSet() |
list.toSet() |
将 List 转为 Set(去重) |
三.常量。
Dart中定义常量也有两种方式,一种使用final关键字,同Java中的用法, 一个 final 变量只能赋值一次;另一种是Dart的方式,使用const关键字定义。
1.Final 修饰符定义常量
1 | void main() { |
2.const修饰符定义常量
1 | void main() { |
3. final 和 const 的区别
const声明时必须赋值,不然会报错,final 声明时可以不用先赋值。
1 | void main() { |
final定义的常量是运行时常量,final声明变量只能赋值一次,并不要求赋的值一定是编译时常量,可以是常量也可以不是。 在程序运行的时候赋值,赋值后值不再改变。
const常量则是编译时常量 要求在声明时初始化,并且赋值必需为编译时常量。
四.流程控制。
1.if和else
1 | if 和 else |
dart的if else 必须是bool型
2.循环 for,while 和 do-while
1 | for (初始语句;条件表达式;结束语句){ |
3.switch case
在 Dart 中 switch 语句使用 == 比较整数,字符串,或者编译时常量。 比较的对象必须都是同一个类的实例(并且不可以是子类), 类必须没有对 == 重写。 枚举类型可以用于 switch 语句。
1 | var command = 'OPEN'; |
4.break continue
break语句可以结束for等代码块。
1 | void breakDemo() { |
continue语句可以结束当前循环,开始下一次的循环迭代过程。
1 | void continueDemo() { |
五.函数。
1.方法定义
1 | 返回类型 方法体 (参数1, 参数2, ...){ |
如果一个函数没有显示声明返回值,Dart 会自动推导它的返回值类型。
1 | // 声明返回值 |
如果一个函数没有显示的 return,那么它默认会返回 null。
1 | fun(){} |
如果一个函数只有一句表达式,可以使用简写成单表达式函数(箭头函数):
1 | bool isMan(String sex) => sex == '男'; |
2.命名参数
顾名思义 就是给参数定了个名字,和kotlin里面的具名函数一样.
通过 {} 符号,可以用于指定函数参数的名称。
1 | void makeHotPot({ |
可以看到,Dart 支持我们给参数设置默认值。
使用 required 修饰的参数,表示必要的参数,在调用的时候你不能遗漏它。
3.位置参数
使用中括号[]括起来的参数是函数的位置参数,代表该参数可传可不传,位置参数只能放在函数的参数列表的最后面。
1 | // 位置参数可以有多个,比如[String a, int b] |
4.匿名函数
多数函数是有名字的, 比如 main()和 print()。 也可以创建没有名字的函数,这种函数被称为 匿名函数, 有时候也被称为 lambda 或者 closure(闭包函数) 。 匿名函数可以赋值到一个变量中
匿名方法看起来与命名方法类似,在括号之间可以定义参数,参数之间用逗号分割。
后面大括号中的内容则为函数体:
1 | ([[类型] 参数[, …]]) { |
注意:参数类型是可以选的,可以不带。
无参数的匿名函数:
1 | // 传入一个函数对象,并执行该函数 |
带参数的匿名函数:
1 | void runFunc(Function(String name) func){ |
用途:
| 用途 | 描述 | 示例 |
|---|---|---|
| 函数参数 | 将函数传递给其他函数,例如回调、事件处理器 | void runFunc(Function func) { func(); } runFunc(() { print("回调执行"); }); |
| 赋值给变量 | 便于动态调用或存储函数逻辑 | var greet = (String name) { print("Hi, $name"); }; greet("张三"); |
| 闭包 | 访问外部变量,实现数据封装和私有状态 | Function makeAdder(int addBy) => (int i) => i + addBy; var add2 = makeAdder(2); print(add2(3)); //5 |
| 集合操作 | 常用于 List.forEach(), map(), where() 等 |
var list = [1,2,3]; list.forEach((e) => print(e*2)); // 2 4 6 |
5.局部函数
在Dart中还有一种可以直接定义在函数体内部的函数,可以把称为局部函数或者内嵌函数。
函数声明可以出现顶层,比如常见的main函数等等。局部函数的好处就是从作用域角度来看,它可以访问外部函数变量,并且还能避免引入一个额外的外部函数,使得整个函数功能职责统一。
1 | //定义外部函数getSoda |
6.顶层函数和静态函数
在Dart中有一种特别的函数,在面向对象语言中比如Java,并不能直接定义一个函数的,而是需要定义一个类,然后在类中定义函数。
但是在Dart中可以不用在类中定义函数,而是直接基于dart文件顶层定义函数,这种函数我们一般称为顶层函数。最常见就是main函数了。
而静态函数就和Java中类似,依然使用static关键字来声明,然后必须是定义在类的内部的。
1 | //顶层函数,不定义在类的内部 |
4.函数作为参数
在 Dart 中,函数本身也是个对象,它对应的类型是Function,这意味着函数可以当做变量的值或者作为一个方法入传参数值。
1 | void sayHello(var name){ |
六. 异常处理
1. try-catch 语法
异常是表示发生了意外的错误,如果没有捕获异常,引发异常的隔离程序将被挂起,并且程序将被终止;Dart可以抛出并捕获异常,但与java相反,Dart的所有异常都是未检查的异常,方法不声明它们可能抛出哪些异常,也不需要捕获任何异常。
1 | try { |
Dart 提供了 try-catch-finally 机制用于捕获和处理运行时异常,同时支持自定义异常和抛出异常。可以通过on 关键词来指定异常类型,finally一般用于释放资源等一些操作,它表示最后一定会执行的意思,即便try…catch中有return,它里面的代码也会承诺执行。
2. 自定义异常
可以通过继承 Exception 或 Error 自定义异常。
1 | class MyException implements Exception { |
3. throw 抛出异常
使用 throw 抛出异常对象。
1 | void validate(int age) { |
4. 异常处理建议
- 捕获具体异常,避免捕获所有异常导致隐藏错误。
- 在 finally 中释放资源(如文件、网络、数据库连接)。
- 自定义异常可以提高代码可读性和可维护性。
- 不要滥用异常,异常处理应仅用于异常场景。
七.类
类的定义
Dart 是一门面向对象的编程语言,所有对象都是某个类的实例,所有类继承了Object类。
- 类的定义用class关键字。
- 如果未显式定义构造函数,会默认一个空的构造函数。
- 类首字母必须大写。
- 使用new关键字和构造函数来创建对象。
一个简单的类:Dart 通过. 来调用类成员变量和方法的。和Kotlin一样还可以通过.?来避免null对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Person {
var name;
var age;
// 构造器
Person(this.name, this.age);
// 实例方法
String getDescribe() {
return "我叫$name我今年$age岁";
}
}
main() {
var person = new Person("张三", "50");
var name = person.name;
var describe = person.getDescribe();
}
类的引入
在要使用的 dart 文件中引入 Person 类:
1 | import 'lib/Person.dart'; |
构造函数
自定义类的默认构造函数:
1 | class Person { |
默认构造函数的简写
1 | class Person { |
命名构造函数
自定义命名函数:
1 | class Person { |
初始化列表
初始化列表就是在对象构造时,构造函数体执行之前,用来给 final 或需要提前赋值的字段进行初始化的地方。
- 初始化列表会在构造方法体执行之前执行。
- 使用逗号分隔初始化列表。
- 初始化列表常用于设置final属性的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Person {
String name;
int age;
final String gender;
Person(this.name, this.age, this.gender);
Person.withMap(Map map) : this.name = map['name'] , this.gender = map['gender'] { //this.gender = map['gender'] 会在构造方法执行前进行赋值,此种方式主要用于final属性的赋值
this.age = map['age'];
}
}
void main() {
var map = {'name': 'jack', 'age': 20, 'gender': '男'};
var p = Person.withMap(map);
print(p.name);
print(p.age);
print(p.gender);
}
类中的getter和setter修饰符
1 | class Rect { |
Dart 中的访问控制(封装)
Dart 与 Java 不同,没有 private、protected、public 关键字,访问控制通过 命名规则 实现。
1. 公有属性和方法(默认)
- Dart 类中的 所有字段和方法默认都是 public。
- 在同一类或其他库中都可以访问。
1 | class Person { |
2. 私有属性和方法(使用下划线 _)
- 规则:在变量或方法名前加
_ - 作用域:仅在 当前 Dart 库(文件) 中可见,跨文件不可访问
1 | // person.dart |
- 注意:Dart 中的 “私有” 是 库级私有,而不是类级私有。
- 也就是说,只要在同一个文件中,其他类也可以访问
_开头的属性或方法。
- 也就是说,只要在同一个文件中,其他类也可以访问
1 | class Teacher { |
3. 小结表
| 修饰方式 | 描述 | 可见范围 |
|---|---|---|
| 默认 | 无修饰符 | 公有,整个项目都可访问 |
_前缀 |
私有变量或方法 | 仅在当前 Dart 库(文件)内可见 |
类中的静态成员 静态方法
使用static关键字来实现类级别的变量和函数。
1 | class Person { |
静态方法只能访问静态属性,不能访问非静态成员。非静态方法可以访问静态属性。
1 | class Person { |
静态变量
- 静态变量(类变量)对于类范围的状态和常量非常有用。
- 静态变量在使用之前不会初始化。
静态方法
静态方法(类方法)不能在实例操作,因此它没有访问this的权限。
为了常用或广泛使用的实用程序和功能,考虑使用顶层函数,而不是静态方法。可以使用静态方法作为编译时常量。例如,可以将静态方法作为参数传递给常量构造函数。
八.类继承
Dart 是单继承语言,一个子类只能继承一个父类,但可以通过 接口(implements) 或 混入(mixin) 来实现多重行为组合。
1. extends 关键字
- 用于创建子类继承父类。
- 子类会继承父类的实例方法、字段和getter/setter。
- Dart 不支持多继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Animal {
String name;
int age;
Animal(this.name, this.age);
void eat() {
print('$name is eating.');
}
}
class Dog extends Animal {
Dog(String name, int age) : super(name, age); // 调用父类构造方法
void eat() {
super.eat(); // 调用父类的方法
print('$name is eating dog food.');
}
}
void main() {
var dog = Dog('Buddy', 3);
dog.eat();
}
说明:
- 使用
super可以调用父类的构造方法或方法。 - 建议在重写父类方法时加上
@override注解,这样更安全,协作时也更清晰。 - 子类构造方法必须调用父类构造方法(如果父类没有无参构造函数)。
2. 重写成员
子类可以重写父类的:
- 实例方法
- getter
- setter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Television {
void turnOn() {
print('Display lights up');
}
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
void _bootNetworkInterface() => print('Network interface started');
void _initializeMemory() => print('Memory initialized');
void _upgradeApps() => print('Apps upgraded');
}
void main() {
var tv = SmartTelevision();
tv.turnOn();
}
3. 重写操作符
Dart 支持通过 operator 重载一些运算符,例如:+、-、[]、== 等。
1 | class Vector { |
4. noSuchMethod()
当调用对象中不存在的成员时,Dart 会调用 noSuchMethod(),可用于动态代理或调试。
1 | class A { |
Invocation对象提供了:memberName:调用的方法或属性名positionalArguments:位置参数列表namedArguments:命名参数列表
5. 总结
| 特性 | 说明 |
|---|---|
| 继承 | 使用extends,单继承 |
| 调用父类方法 | 使用super.method() |
| 调用父类构造 | 使用super(...) |
| 重写方法/属性 | 使用@override注解 |
| 重载运算符 | 使用operator关键字 |
| 捕获未定义成员 | 重写noSuchMethod() |
九.抽象类和接口
抽象类
Dart 的抽象类和Java差不多,除了抽象类是不能被实例化的,可以声明抽象方法之外,和一般类没有区别。抽象类不能实例化,可以当做抽象类来 extends 也可以当做接口来 implements,dart 中没有 interface 这个关键字,接口也是抽象类实现的。
1 | abstract class Animal { |
隐式的接口
在 Dart 中,没有显式的 interface 关键字,但每个类都可以被当作接口使用。通过 implements 可以实现类的接口,从而支持多态和灵活的代码组合。
1 | // 每个类都是一个隐式的接口,所以Person类也是个接口,包括成员属性和方法. |
实现多个接口
1 | class Point implements Comparable, Location {...} |
实现接口的规则
- 必须重写接口中的所有公有成员(方法和字段)。
- 构造函数不继承,实现接口的类必须自己定义构造函数。
- 支持多接口实现,用逗号分隔多个接口。
Mixin混入
在通过implements实现某个类时,类中所有的方法都必须被重新实现 (无论这个类原来是否已经实现过该方法)。但是某些情况下,一个类可能希望直接复用之前类的原有实现方案,怎么做呢?
使用继承吗?但是Dart只支持单继承,那么意味着你只能复用一个类的实现。 Dart提供了另外一种方案: Mixin混入的方式:
除了可以通过class定义类之外,也可以通过mixin关键字来定义一个类。只是通过mixin定义的类用于被其他类混入使用,通过with关键字来进行混入。
- Mixin 是一种 类的复用方式,用于把已有类的功能“混入”到另一个类中。
- 与继承不同,混入不会改变类的继承层次,也不需要实现接口。
- Mixin 可以包含方法、属性,但通常不包含构造函数。Mixin 的限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24main(List<String> args) {
var superMan = SuperMan();
superMan.run();
superMan.fly();
}
mixin Runner {
run() {
print('在奔跑');
}
}
mixin Flyer {
fly() {
print('在飞翔');
}
}
// implements的方式要求必须对其中的方法进行重新实现
// class SuperMan implements Runner, Flyer {}
class SuperMan with Runner, Flyer {
} - 不能有构造函数
- 可以混入多个 Mixin
- 可以约束 Mixin 使用范围(Dart 2.1+ 支持 on 关键字)
Mixin vs 继承 vs 接口
| 特性 | 继承 | 接口 | Mixin |
|---|---|---|---|
| 复用方式 | 单继承 | 实现接口 | 混入功能 |
| 方法实现 | 可以继承 | 必须重写 | 可直接使用 |
| 构造函数 | 可以有 | 无构造函数 | 不能有 |
| 多重使用 | 单继承 | 可多接口 | 可多混入 |
| 作用 | 类型体系 | 强制约束 | 功能复用 |
Mixin 就像“能力模块”,可以给任意类混入额外功能,而不影响继承关系,使代码复用更加灵活和组合化。
十. 枚举
枚举类型,通常称为枚举,是一种特殊类型的类,用于表示固定数量的常量值。
使用枚举
使用enum关键词来声明一个枚举类型。
1 | enum Color { red, green, blue } |
枚举中的每个值都有一个index索引,它返回枚举声明中值的从零开始的位置。例如,第一个值具有索引0,第二个值具有索引1。
1 | print(Color.red.index == 0);//true |
若要获取枚举中所有值的列表,请使用枚举的values常量。
1 | List<Color> colors = Color.values; |
你可以在switch语句中使用枚举,如果不处理枚举的所有值,将会收到警告:
1 | var aColor = Color.blue; |
枚举类型有以下限制:
- 不能子类化、混合或实现枚举。
- 不能显式实例化枚举。
十一. 泛型
泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明。
从字面的意思理解来看,泛型,泛就是模糊、暂不确定暂定的意思。可以这样理解,使用泛型就是,定义的一个类型,类型暂不确定,给使用给一个占位符给代替,在使用的时候可以给确定其定义的类型。
泛型方法
泛型方法可以约束一个方法使用同类型的参数、返回同类型的值,可以约束里面的变量类型。
1 | T getData<T> (T val) { |
泛型类
声明泛型类,比如声明一个 Array 类,实际上就是 List 的别名,而 List 本身也支持泛型的实现。
1 | class Array<T> { |
使用泛型类:
1 | List l1 = new List<String>(); |
泛型接口
下面声明了一个 Storage 接口,然后 Cache 实现了接口,能够约束存储的 value 的类型:
1 | abstract class Storage<T>{ |
使用泛型接口实现的类:
1 | Cache ch = new Cache<String>(); |
十二.使用类库
有生命力的编程语言,它背后都有一个强大的类库,它们可以让我们站在巨人的肩膀上,又免于重新造轮子。
导入类库
在Dart里面,通过import关键词来导入类库。
内置的类库使用dart:开头引入:
1 | import 'dart:io'; |
第三方类库或者本地的dart文件用package:开头:
比如导入用于网络请求的dio库:
1 | import 'package:dio/dio.dart'; |
Dart 应用本身就是一个库,比如我的应用名是blog,导入其他文件夹的类:
1 | import 'package:blog/common/net_utils.dart'; |
Dart 通过pub.dev来管理类库,类似Java世界的Maven 或者Node.js的npm一样,你可以在里面找到非常多实用的库。
解决类名冲突
如果导入的类库有类名冲突,可以通过as使用别名来避免这个问题:
1 | import 'package:lib1/lib1.dart'; |
导入部分类
在一个dart文件中,可能会存在很多个类,如果你只想引用其中几个,你可以增加show或者hide来处理:
1 | //文件:my_lib.dart中 |
也可以使用hide排除Three,和上面是等价的:
1 | //文件:test.dart中 |
十三.异步
开发中的耗时操作:
在开发中,我们经常会遇到一些耗时的操作需要完成,比如网络请求、文件读取等等;如果我们的主线程一直在等待这些耗时的操作完成,那么就会进行阻塞,无法响应其它事件。
处理耗的操作有两种方式:
- 处理方式一: 多线程,比如Java、C++,我们普遍的做法是开启一个新的线程(Thread),在新的线程中完成这些异步的操作,再通过线程间通信的方式,将拿到的数据传递给主线程。
- 处理方式二: 单线程+事件循环,比如JavaScript、Dart都是基于单线程加事件循环来完成耗时操作的处理。
Dart是一个单线程编程语言。在Dart的世界里没有多线程之说,当然也没有了所谓的主线程和子线程之分。如果任何代码阻塞线程执行都会导致程序卡死。
阻塞式调用和非阻塞式调用
- 阻塞式调用: 调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行。
- 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。
单线程模型
在Java中使用多线程来处理并发任务,适量并合适地使用多线程,能够极大地提高资源的利用率和程序运行效率,但是缺点也比较明显,比如过度开启线程会带来额外的资源和性能消耗或多线程共享内存容易出现死锁等。 因此又出现了基于事件的异步模型。简单说就是在某个单线程中存在一个事件循环和一个事件队列,事件循环不断的从事件队列中取出事件来执行,这里的事件就好比是一段代码,每当遇到耗时的事件时,事件循环不会停下来等待结果, 它会跳过耗时事件,继续执行其后的事件。当不耗时的事件都完成了,再来查看耗时事件的结果。因此,耗时事件不会阻塞整个事件循环,这让它后面的事件也会有机会得到执行。
Dart是一种单线程语言,因此Dart程序没有主线程和子线程之分,Dart是通过消息循环(Event Looper)和事件队列(Event queue)来进行异步操作的.Dart在一条执行线上,同时且只能执行一个任务(事件),其他任务都必须在后面排队等待被执行。也就是说,在一条执行线上,为了不阻碍代码的执行 ,每遇到的耗时任务都会被挂起放入任务队列,待执行结束后再按放入顺序依次执行队列上的任务,从而达到异步效果。
Dart事件循环机制是由一个 消息循环(Event looper) 和两个消息队列:事件队列(Event queue) 和 微任务队列(MicroTask queue) 构成。

从上图可知,Dart事件循环机制由一个消息循环(event looper)和两个消息队列构成,其中,两个消息队列是指事件队列(event queue)和微任务队列(Microtask queue)。该机制运行原理为:
- 首先,Dart程序从main函数开始运行,待main函数执行完毕后,event looper开始工作。
- 然后,event looper优先遍历执行Microtask队列所有事件,直到Microtask队列为空。
- 接着,event looper才遍历执行Event队列中的所有事件,直到Event队列为空。
- 最后,视情况退出循环。
注意: Dart中使用阻塞式调用也会造成程序卡死.
Future
Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future。从名字上也很容易理解,它表示一个在未来时间才会完成的任务。Future 是一个延后计算的对象,即它的返回值当前并不一定可用,在未来某个时刻它完成计算后便会返回可用的值。比如一个网络请求。通常使用 Future.then 来处理计算完成的场合,用 Future.catchError 来处理发生异常的场合。
Future是用于自定义Event queue事件。通过创建Future类实例来向Event queue添加事件:
1 | new Future(() { |
then
创建完成Future对象后,可以通过then方法接收Future的结果。
1 | import "dart:io"; |
执行结果:
1 | main() start |
Future的链式调用,可以在then中继续返回值,会在下一个链式的then调用回调函数中拿到返回的结果。
1 | import "dart:io"; |
执行结果:
1 | main() start |
catchError
如果Future内的函数执行发生异常,可以通过Future.catchError来处理异常:
1 | import "dart:io"; |
async 和 await 关键字
async 和 await 关键字用于支持 Dart 语言的异步特性。async用来修饰方法,需要写在方法括号的后面, 它的调用者并不会等待它执行完毕。而 await 关键字必须存在于 async 方法内,被标为await的语句一般为耗时操作, 它后面的语句会等待 await 语句执行完毕。当耗时操作完成时,await后面的代码便会得到执行(异步)。这对关键字的存在意义就是以同步的编程风格,实现异步的执行。await表达式可以使用多次。 有了这两个关键字,我们可以更简洁的编写异步代码,而不需要调用Future相关的API。
将 async 关键字作为方法声明的后缀时,具有如下意义:
- 被修饰的方法会将一个 Future 对象作为返回值
- 该方法会同步执行其中的方法的代码直到第一个 await 关键字,然后它暂停该方法其他部分的执行;
- 一旦由 await 关键字引用的 Future 任务执行完成,await的下一行代码将立即执行。
1 | // 导入io库,调用sleep函数 |
运行结果:
1 | main() start |
需要注意,async 不是并行执行,它是遵循Dart 事件循环规则来执行的,它仅仅是一个语法糖,简化Future API的使用。
轻量异步任务
对于一些轻量异步任务,比如一个小的网络请求,本身的计算量不大,只是我们不知道它的确切完成时间。这种情况我们用 async 和 await 来简单创建一个异步任务即可。
这个异步任务并没有创建新线程,只是通过语言机制达到了异步执行而已。一个简单的例子:
1 | Future<String>( () async { |
输出:
1 | 这里没有等待 |
从输出我们看到一个异步执行机制, 异步方法后面的语句立即得到执行,5秒后,再输出模拟计算的结果。这里的延时5秒实际并没有占用多少CPU资源,所以它属于轻量计算。 在实际测试中,轻量的异步计算并不会导致UI卡顿。到底多少计算量会导致UI卡顿?一般情况下,如果真实的CPU计算耗时超过10毫秒,就有卡顿风险了。那时就需要创建线程了。
Isolate
大多数计算机中,甚至在移动平台上,都在使用多核CPU。 为了有效利用多核性能,开发者一般使用共享内存数据来保证多线程的正确执行。 然而多线程共享数据通常会导致很多潜在的问题,并导致代码运行出错。 Dart作为一种新语言,为了缓解上述问题,提出了Isolate(隔离区)的概念,即Dart没有线程的概念,只有Isolate,所有的Dart代码都是在Isolate中运行, 它就像是机器上的一个小空间,具有自己的私有内存堆和一个运行着Event Looper的单个线程。 它与线程最大的区别就是不能共享内存,因此也不存在锁竞争问题,两个Isolate完全是两条独立的执行线,且每个Isolate都有自己的事件循环,它们之间使用 Port 和 Message 来发送消息通信,所以它的资源开销低于线程。 Isolate 可执行于不同CPU核心来提高性能。
默认情况下,Dart程序只有一个Isolate(未自己创建的情况下),而这个Isolate就是Main Isolate。也就是说,一个Dart程序是从Main Isolate的main函数开始的,而在main函数结束后 ,Main isolate线程开始一个一个处理事件循环模型队列中的每一事件(Event)。
使用场景
在 Dart 中 async 和 Future 无法解决所有耗时的工作。Dart 虽然支持 异步执行,但其实如果是通过 async 的话,只是把工作丟到同一个 event loop 中, 让他暂时不会卡住目前的工作 , 等到真的轮到它执行的时候 , 如果它真的很耗时,那 main isolate 还是会 freeze(冻结) 住的 (为什么会冻结? 主线程负责 UI的渲染 工作 但是 如果 密集型计算 很耗时 假如 这个计算 占用 1s的时间 你的UI就会卡住1s) 。
Dart 主要的 task 都是在 main isolate 中完成的,isolate 像是个 single thread 的 process。如果真的想要让某些工作能夠同时执行,不要卡住 main isolate 的话,就得要自己产生新的 isolate 來执行。
创建Isolate
需要导入 isolate
1 | import 'dart:isolate'; |
创建Isolate是比较简单的,可以使用 Isolate.spawn() 或 Flutter的 compute() 函数创建单独的隔离区来进行消耗性能的计算。
新创建的隔离区拥有自己的事件循环和内存,即使原始隔离区是该新隔离区的父级,也不允许其访问。
Isolate.spawn 必须传入一个有且仅有一个参数的函数,不可以不传。
1 | import 'dart:isolate'; |
当我们调用 Isolate.spawn 的时候,它将会返回一个对 isolate 的引用的 Future。我们可以通过这个 isolate 来控制创建出的 Isolate,例如 pause、resume、kill 等等。
1 | import 'dart:io'; |
Isolate通信机制
但是在真实开发中,我们不会只是简单的开启一个新的Isolate,而不关心它的运行结果:
- 我们需要新的Isolate进行计算,并且将计算结果告知Main Isolate(也就是默认开启的Isolate)。
- Isolate 通过发送管道(SendPort)实现消息通信机制。
- 我们可以在启动并发Isolate时将Main Isolate的发送管道作为参数传递给它。
- 并发在执行完毕时,可以利用这个管道给Main Isolate发送消息。
1 | import 'dart:io'; |
双向通信:
- 事实上双向通信的代码会比较麻烦。
- Flutter提供了支持并发计算的compute函数,它内部封装了Isolate的创建和双向通信。
- 利用它我们可以充分利用多核心CPU,并且使用起来也非常简单。
1 | main(List<String> args) async { |
注意:上面的代码不是dart的API,而是Flutter的API,所以只有在Flutter项目中才能运行。
Future和isolate选择
Isolate 实际上是比较重的,每当我们创建出来一个新的 Isolate 至少需要 2mb 左右的空间甚至更多,取决于我们具体 isolate 的用途。那么应该在什么时候使用Future,什么时候使用Isolate呢? 其实这个问题,更值得去注意,因为这是和实际的开发直接相关,有时候确实需要知道什么时候应该是 Future ,什么时候应该使用 isolate . 有的人说使用 isolate 比较重,一般不建议采用,其实不能这样一概而论。 isolate 也是有使用场景的,有些人会疑惑网络请求应该算耗时吧,平时一般使用 Future 就够了。
最简单的判断方法是根据某些任务的平均时间来选择:
- 方法执行在几毫秒或十几毫秒左右的,应使用Future。
- 如果一个任务需要几百毫秒或之上的,则建议创建单独的Isolate。
换句话说,建议尽可能多地使用 Future (直接或间接通过异步方法),因为一旦 EventLoop 有空闲期,这些 Future 的代码就会运行。
- 如果一段代码不会被中断,那么就直接使用Future就行。
- 如果代码段可以独立运行而不会影响应用程序的流畅性,建议使用 Future。
- 如果繁重的处理可能要花一些时间才能完成,而且会影响应用程序的流畅性,建议使用 isolate。
下面列出一些使用 isolate 的具体场景:
- JSON大数据解析: 解码大JSON数据。
- 加解密: 加解密过程比较耗时
- 图片处理: 比如裁剪图片比较耗时
- 从网络中加载大图
参考资料:
Dart语法篇之函数的使用(四)
Flutter Dart 语言基础详解
【dart学习】– Dart之异步编程
Dart语法