1. Dart简介
Dart介绍:
Dart是由谷歌开发的计算机编程语言,它可以被用于web、服务器、移动应用 和物联网等领域的开发。
Dart诞生于2011年,号称要取代JavaScript。但是过去的几年中一直不温不火。直到Flutter的出现现在被人们重新重视。
要学Flutter的话我们必须首先得会Dart。
官网:dart.dev/
1.1. Dart环境安装
中文官网:dart.cn/,直接去官网下载 Dart SDK 。
但是不干 Flutter 开发,谁学 Dart 啊,Flutter SDK是包含 Dart SDK的,反正后面也是要学习 Flutter,直接安装 Flutter SDK 得了。
→安装 Flutter SDK(opens new window)
下面是单独安装 Dart SDK:dart.dev/get-dart
1.1.1. Windows系统
方式一:gekorm.com/dart-window… 下载下来一个exe直接安装就可以了
方式二:(下面这些都是)
1.1.1.1.1. 下载安装包
这里直接通过下载链接来下载,选择的是稳定的版本,如果要修改为其他版本,修改链接中的版本号就可以了:
https://storage.flutter-io.cn/dart-archive/channels/stable/release/2.19.6/sdk/dartsdk-windows-x64-release.zip
1.1.1.1.2. 安装
下载的zip,直接找个地方解压即可。
1.1.1.1.3. 配置环境变量
- 打开“控制面板”。
- 选择“系统和安全”。
- 选择“系统”。
- 选择“高级系统设置”。
- 点击“环境变量”。
- 在“用户变量”中,单击“新建”。
- 在“变量名”中,输入“DART_SDK”。
- 在“变量值”中,输入Dart SDK的安装路径。
- 单击“确定”。
1.1.1.1.4. 验证安装
打开命令提示符,在命令行中输入:
dart --version
1
能查看到 dart 的版本。
1.1.2. Mac系统
1.1.2.1.1. 先安装Homebrew
下面这句命令复制到终端
上面执行完成之后会有个Next steps,去执行一下才能有brew命令
echo >> /Users/lixiaofeng/.zprofile
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/lixiaofeng/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
1.1.2.1.2. 执行命令
brew tap dart-lang/dart
brew install dart
1.1.2.1.3. 验证安装
重启命令行,然后输入:
dart --version
能查看到 dart 的版本。
1.2. Dart开发工具
1.2.1. vscode
这里使用 Visual Studio Code,也就是 VS Code 来作为开发功能。
VS Code的安装,直接下载安装即可,这里不介绍了。
下面安装两个 VS Code 的插件:
Dart
Code Runner
1.2.2. webstrom
/opt/homebrew/opt/dart/libexec
1.3. HelloWorld
下面我们来编写第一个Hello World程序。
在编写代码之前,先做一些准备工作,准备一个工作空间。
1.3.1. 创建项目
其实这里就是创建一个文件夹,例如我们在电脑中创建了一个 hello-dart 的文件夹,后面我们在这个文件夹中创建文件。
然后使用 VS Code 打开这个文件夹。
1.3.2. 创建Dart文件
项目已经创建好了,然后我们在项目下新建一个 dart 的文件:
输入文件名,以 .dart 结尾。
1.3.3. 编写Hello World程序
在创建的文件中编写我们的Hello World程序:
void main(List<String> args) {
print("Hello World");
}
1.3.4. 运行Dart文件
编写代码完成,点击右上角的“运行”按钮,可以直接运行程序
在下面的 TERMINAL 终端,会显示执行结果。
1.3.5. Hello World 程序解析
void main(List<String> args) {
print("Hello World");
}
- 上面的 main 是一个函数,是程序的入口函数,程序启动,会从这里开始执行;
void表示函数是没有返回值的,返回值是可以省略的;List<String> args表示函数的参数,main 函数的参数可以省略。print("Hello World");表示将字符串 "Hello World" 打印到屏幕上。- 每行语句必须使用分号结尾,很多语言并不需要分号,比如Swift、JavaScript;
关于函数后面会讲解,这里先按照上面固定的写法来写就可以了,无需纠结。
因为 main 函数的参数和返回值可以省略,所以 main 函数可以写成如下:
main() {
print("Hello World");
}
2. 基础语法
2.1. 入口方法
main / void main
main(){
print("你好 dart");
}
void main(){
print("你好 dart")
}
2.2. 输出语句
stdout.write:不会换行,只是把当前的内容用文本方式输出到控制台
print:自动加了一个换行符,输出的时候会换行
print("print text");
stdout.write("stdout text111 - ");
stdout.write("stdout text222");
2.3. 数据类型
Dart是一种强类型语言,不预先设定变量类型情况下可以自动进行类型推导。以下是Dart中支持的数据类型:
基本数据类型:
- 数值型:int(整数)、double(浮点数)
- 布尔型:bool
- 字符串:String
容器:
- 列表:List
- 集合:Set
- 映射:Map
容器:可以存储多种数据的一种数据类型,容器内的每个数据称为元素,其中的元素可以是任意类型的数据,包括字符串、数字、布尔甚至是容器(List,Set,Map...)等等
2.3.1. 数值类型(num)
2.3.1.1. int
必须是整型
int a=123;
a=45;
print(a); //45
2.3.1.2. double
可以是整型也可以是浮点型
double b=23.5;
b=24;
print(b);
2.3.1.3. 运算符
// + - * / %
var c=a+b;
print(c);
2.3.1.4. 浮点数格式化
.toStringAsFixed(2)
double num = 1234.73456;
String message = "浮点数格式优化:${num.toStringAsFixed(2)}";
print(message);//浮点数格式优化:1234.73
2.3.2. 字符串类型
字符串(string),又称文本,是由任意数量的字符如中文、英文、各类符号、数字等组成。
2.3.2.1. 字符串的定义方式
方式一:双引号
var text1 = "我是一个字符串"
方式二:单引号
var text1 = '我是一个字符串'
方式三:三引号定义法
使用三引号定义字符串,可以换行
var str1 = 'this is dart'
String str1 = "this is dart"
String str1 = '''jjahsdkjah
kajhdjkashd
kjasdjasdh''';
print(str1);
'123' // 这个是字符串,不是数字
"abc"
"""
你好,China
"""
2.3.2.2. 转义字符
方式一:双引号包单引号
String str = "Hello 'Dart'";
方式二:单引号包双引号
String str = 'Hello "Dart"';
方式三:转义符``
String str = "hello "Dart""
如果想要在str里面使用\t这样的转义字符,也要转义一下子
String str = "Hello \t "Dart"";
2.3.2.3. 字符串拼接
第一种:插值表达式${}
第二种:加号拼接str1+str2
String str1 = '你好'
String str2 = ’dart‘
print(${str1 str2})
注意:其它的和字符串拼起来可以使用.toString来转成字符串之后再拼接。
2.3.2.4. 判断字符串非空
.isEmpty 和 .isNotEmpty
String str = "";
print(str.isEmpty); // 输出: true
print(str.isNotEmpty); // 输出: false
2.3.2.5. 浮点数格式化(toStringAsFixed)
.toStringAsFixed(2)
double num = 1234.73456;
String message = "浮点数格式优化:${num.toStringAsFixed(2)}";
print(message);//浮点数格式优化:1234.73
2.3.3. 布尔(bool)
值只有2个,即 true 和 false,也就是真和假。
需要注意一下,在dart里面使用==是不会隐式转换的
//dart中这样写是不相等
var a = 123;
var b = '123';
print(a == b); //false
//js中这样写是相等的
var a = 123;
var b = '123'
console.log(a == b) //true
console.log(a === b) //false
2.3.4. List(列表)
- 元素可以重复
- 可以增删改查
- 有序,可以通过索引访问
- 可以存储不同类型的数据
2.3.4.1. 创建
var emptyList = [];
var emptyList = [1, 2, 3, 4, 5];
var list2 = <String>["a", "b", "c"];
var list6 = List.filled(3, true);
var list7 = List<String>.filled(2, "");
List<String> list8 = List.filled(5,"哈哈")
2.3.4.2. 添加元素
add(value): 在列表末尾添加一个元素addAll(iterable): 在列表末尾添加多个元素insert(index, value): 在指定位置插入一个元素insertAll(index, iterable): 在指定位置插入多个元素
void main() {
var list = [1, 2, 3];
list.add(4); // [1, 2, 3, 4]
list.addAll([5, 6]); // [1, 2, 3, 4, 5, 6]
list.insert(1, 10); // [1, 10, 2, 3, 4, 5, 6]
list.insertAll(2, [20, 30]); // [1, 10, 20, 30, 2, 3, 4, 5, 6]
print(list);
}
2.3.4.3. 删除元素
remove(value): 删除匹配的第一个元素removeAt(index): 删除指定索引的元素removeLast(): 删除最后一个元素removeWhere(test): 删除满足条件的所有元素clear(): 清空列表
void main() {
var list = [1, 2, 3, 4, 5, 6];
list.remove(3); // [1, 2, 4, 5, 6]
list.removeAt(1); // [1, 4, 5, 6]
list.removeLast(); // [1, 4, 5]
list.removeWhere((element) => element % 2 == 0); // 删除所有偶数 [1, 5]
print(list);
}
2.3.4.4. 修改元素
- 通过索引直接赋值
replaceRange(start, end, iterable): 替换指定范围内的元素
void main() {
var list = [1, 2, 3, 4, 5];
list[2] = 30; // [1, 2, 30, 4, 5]
list.replaceRange(1, 3, [100, 200]); // [1, 100, 200, 4, 5]
print(list);
}
2.3.4.5. 遍历元素
for-in循环forEach****迭代器:遍历列表map((e) => newValue).toList(): 生成新列表where((e) => condition).toList(): 过滤元素expand((e) => iterable).toList(): 展开列表any(e => condition):判断是否有符合条件的元素every(e=>condition):判断是否所有的元素都符合条件
List<String> fruits = ['apple', 'banana', 'orange'];
for (var fruit in fruits) {
print(fruit);
}
fruite.forEach(v => {
print(v)
})
Iterator<String> iterator = fruits.iterator;
while (iterator.moveNext()) {
print(iterator.current);
}
//apple banana orange
var list = [1, 2, 3, 4, 5];
var squared = list.map((e) => e * e).toList(); // [1, 4, 9, 16, 25]
print(squared);
var evens = list.where((e) => e.isEven).toList(); // [2, 4]
print(evens);
var expanded = list.expand((e) => [e, e * 10]).toList();
// [1, 10, 2, 20, 3, 30, 4, 40, 5, 50]
print(expanded);
2.3.4.6. 其它操作
isEmpty():判断是否为空
isNotEmpty():判断是否不为空
contains():检查是否包含某个元素
join():将元素拼接成字符串
split():将字符串分割成一个List
2.3.4.7. 示例代码
void main() {
//第一种方式:var定义,自动识别类型,长度可以变化
var list1 = [1, "张三", true];
print(list1); //[1, "张三",true]
list1.add(5);
print(list1); //[1, "张三",true,5]
list1.length = 0;
print(list1); //[]
//第二种方式:var定义并指定类型,长度可以变化
var list2 = <String>["a", "b", "c"];
print(list2); //["a", "b", "c"]
list2.add("aaa");
print(list2); //["a", "b", "c","aaa"]
var list3 = <int>[1, 2, 90];
print(list3); //[1, 2, 90]
list3.remove(90);
print(list3); //[1,2]
//第三种方式:var定义,直接给一个[],容量可以变化
var list4 = [];
list4.addAll(["123", 345, true]);
print(list4); //["123", 345, true]
//第四种方式:new List方式(新版本中的dart里面没有使用这个方法了)
var list5 = new List();
//第五种方法:List.filled(个数,默认值);不可改变长度
var list6 = List.filled(3, true);
print(list6); //[true,true,true]
list6.add(true); //报错:Unsupported operation: Cannot add to a fixed-length list
print(list6);
第六种方法:固定类型+List.filled
var list7 = List<String>.filled(2, "");
print(list7); //[,]
list7.add("string"); //报错
print(list7);
}
2.3.5. Set(集合)
- 不允许重复
- 无序,不能通过下标来访问元素
- 集合可被修改
- 可以存储不同的数据类型
2.3.5.1. 创建
var emptySet = {};
Set setEle = {123,"haha",true};
Set<String> emptySet = {};
Set<String> fruits = {'apple', 'banana', 'orange'};
Set<String> fruits = Set<String>();
Set<String> fruits = Set<String>.from(['apple', 'banana', 'orange']);
Set<String> fruits = Set<String>.from({'apple', 'banana', 'orange'});
2.3.5.2. 添加元素
add(value): 添加一个元素addAll(iterable): 添加多个元素
var mySet = <int>{1, 2, 3};
mySet.add(4); // {1, 2, 3, 4}
mySet.addAll([5, 6, 7]); // {1, 2, 3, 4, 5, 6, 7}
print(mySet);
2.3.5.3. 删除元素
remove(value): 删除指定的元素,若元素不存在则返回falseremoveWhere(test): 删除满足条件的元素clear(): 清空所有元素
var mySet = {1, 2, 3, 4, 5, 6};
mySet.remove(3); // {1, 2, 4, 5, 6}
mySet.removeWhere((e) => e % 2 == 0); // 删除所有偶数 {1, 5}
print(mySet);
mySet.clear(); // 清空
print(mySet); // {}
2.3.5.4. 检查元素
contains(value): 检查元素是否存在containsAll(iterable): 检查多个元素是否都存在
var mySet = {1, 2, 3, 4, 5};
print(mySet.contains(3)); // true
print(mySet.contains(10)); // false
print(mySet.containsAll([1, 2])); // true
2.3.5.5. 遍历元素
forEach((element) => action): 遍历所有元素map((e) => newValue).toSet(): 变换元素生成新Setwhere((e) => condition).toSet(): 筛选符合条件的元素
var mySet = {1, 2, 3, 4, 5};
mySet.forEach((e) => print("元素: $e"));
var squaredSet = mySet.map((e) => e * e).toSet();
print(squaredSet); // {1, 4, 9, 16, 25}
var evenSet = mySet.where((e) => e.isEven).toSet();
print(evenSet); // {2, 4}
2.3.5.6. 集合运算
union(otherSet): 并集intersection(otherSet): 交集difference(otherSet): 差集
var setA = {1, 2, 3, 4};
var setB = {3, 4, 5, 6};
print(setA.union(setB)); // 并集 {1, 2, 3, 4, 5, 6}
print(setA.intersection(setB)); // 交集 {3, 4}
print(setA.difference(setB)); // 差集 {1, 2}
2.3.5.7. 长度 & 判空 & 固定长度
isEmpty: 判断是否为空isNotEmpty: 判断是否非空length: 获取元素数量Set没有固定长度的实现,不过可以使用Set.unmodifiabel来创建不可变集合
var mySet = {1, 2, 3};
print(mySet.length); // 3
print(mySet.isEmpty); // false
print(mySet.isNotEmpty); // true
var immutableSet = Set.unmodifiable({1, 2, 3});
print(immutableSet); // {1, 2, 3}
// immutableSet.add(4);
// 运行时错误: Cannot modify an unmodifiable set
2.3.6. Map(字典)
2.3.6.1. 创建
var map1 = {'name': 'Alice', 'age': 25}; // 字面量方式
var map2 = Map<String, dynamic>(); // 构造函数方式
// 创建一个空Map
Map<String, String> map = {};
// 创建一个包含元素的Map
Map<String, int> map = {'zhangsan': 94, 'lisi': 96, 'wangwu': 91};
// 使用Map的构造函数来创建一个空集合
Map<String, String> map = Map<String, String>();
2.3.6.2. 添加/修改元素
- 直接使用
map[key] = value来添加或修改键值对。 addAll(map): 添加多个键值对。update(key, (oldValue) => newValue): 更新指定键的值。update(key, (oldValue) => newValue, ifAbsent: () => defaultValue): 如果键不存在,添加默认值。
ar person = {'name': 'Alice', 'age': 25};
person['city'] = 'New York'; // 添加新键值对
person['age'] = 26; // 修改已有的键值对
person.addAll({'country': 'USA', 'gender': 'Female'});
person.update('age', (old) => old + 1); // 更新 age: 27
person.update('height', (old) => old, ifAbsent: () => 170); // 添加新键 height: 170
print(person); // {name: Alice, age: 27, city: New York, country: USA, gender: Female, height: 170}
2.3.6.3. 删除元素
remove(key): 删除指定键的元素,并返回被删除的值。removeWhere((key, value) => condition): 删除符合条件的键值对。clear(): 清空Map。
var person = {'name': 'Alice', 'age': 25, 'city': 'New York'};
person.remove('city'); // 删除 city
print(person); // {name: Alice, age: 25}
person.removeWhere((key, value) => key.startsWith('a')); // 删除所有键以 'a' 开头的元素
print(person); // 仍然 {name: Alice, age: 25}
person.clear(); // 清空 Map
print(person); // {}
2.3.6.4. 访问元素
map[key]: 获取指定键的值,若键不存在返回null。containsKey(key): 判断Map是否包含某个键。containsValue(value): 判断Map是否包含某个值。keys: 获取所有键的列表。values: 获取所有值的列表。
void main() {
var person = {'name': 'Alice', 'age': 25, 'city': 'New York'};
print(person['name']); // Alice
print(person['gender']); // null(不存在)
print(person.containsKey('age')); // true
print(person.containsValue('Alice')); // true
print(person.keys.toList()); // [name, age, city]
print(person.values.toList()); // [Alice, 25, New York]
}
2.3.6.5. 过滤 & 转换 & 遍历
orEach((key, value) => action): 遍历键值对。entries: 以MapEntry的形式遍历键值对。map((key, value) => MapEntry(newKey, newValue)): 映射成新Map。where((key, value) => condition): 过滤符合条件的键值对。
void main() {
var person = {'name': 'Alice', 'age': 25, 'city': 'New York'};
person.forEach((key, value) {
print('$key: $value');
});
for (var entry in person.entries) {
print('${entry.key} -> ${entry.value}');
}
var newMap = person.map((key, value) => MapEntry(key.toUpperCase(), value.toString()));
print(newMap); // {NAME: Alice, AGE: 25, CITY: New York}
var numbers = {'one': 1, 'two': 2, 'three': 3, 'four': 4};
var squaredNumbers = numbers.map((key, value) => MapEntry(key, value * value));
print(squaredNumbers); // {one: 1, two: 4, three: 9, four: 16}
var evenNumbers = numbers.entries.where((entry) => entry.value.isEven).toList();
print(evenNumbers); // [(two, 2), (four, 4)]
}
2.3.6.6. 长度 & 判空 & 不可变Map
length: 获取键值对数量。isEmpty: 判断是否为空。isNotEmpty: 判断是否非空。Map.unmodifiable()创建一个不可变的Map
void main() {
var map = {'a': 1, 'b': 2};
print(map.length); // 2
print(map.isEmpty); // false
print(map.isNotEmpty); // true
var immutableMap = Map.unmodifiable({'name': 'Alice', 'age': 25});
print(immutableMap); // {name: Alice, age: 25}
// immutableMap['city'] = 'New York'; // 运行时错误:无法修改不可变 Map
}
2.4. 注释
我们在学习任何语言,都会有注释,注释的作用就是向别人解释我们编写的代码的含义和逻辑,使代码有更好的可读性,注释不是程序,是不会被执行的。
注释一般分类两类,单行注释和多行注释。
2.4.1. 单行注释
单行注释以 // 开头, // 号右边为注释内容。
例如:
// 我是单行注释,打印Hello World
print("Hello World!")
注意:// 号和注释内容一般建议以一个空格隔开,这是代码规范,建议大家遵守。
单行注释一般用于对一行或一小部分代码进行解释。
2.4.2. 多行注释
多行注释是以**/* */**括起来,中间的内容为注释内容,注释内容可以换行。
/*
我是多行注释,
我来哔哔两句
*/
print("哔~~")
多行注释一般对:类、方法进行解释,类和方法后面我们再学习。
2.5. 变量和常量
变量是在程序运行的时候存储数据用的,可以想象变量为一个盒子。
整数、浮点数(小数)、字符串都可以放在变量中。
const 和 final
2.5.1. 定义变量
2.5.1.1. 确定类型
void main() {
String name = "zhangsan"; // 定义一个变量,用来存储姓名
int age = 18;
double height = 1.78;
name = "李四"; // 修改变量的值
age = 19;
print(name);
print(age);
print(height);
}
分别使用 String 来定义字符串、int 来定义整数、double 来定义浮点数。
2.5.1.2. 使用var定义变量
在 dart 中可以使用 var 来替代具体类型的声明,会自动推导变量的类型。
例如:
void main(List<String> args) {
var name = "zhangsan"; // 定义一个变量,用来表示姓名
var age = 18;
var height = 1.78;
name = "李四"; // 修改变量的值
// name = 123; // 错误, name是字符串类型的,无法赋值其他类型数据
age = 19;
print(name);
print(age);
print(height);
}
需要注意, var name="zhangsan"; ****已经确定了变量name的类型为字符串,无法使用 ****name = 123; ****赋值其他类型数据。
2.5.2. 定义常量
变量的值是可以变的,就像上面我们定义了变量,后面可以修改它的值。
常量就是值不能改变的。
在 Dart 中,使用 final 或 const 关键字来定义常量。
🔴注意:final是只能给他赋一次值,但是定义成数组时候是可以往里面添加值的
final a = 1; a = 2; //这样是肯定会报错的
final a = []; a.add(1); //这样是不会报错的
两者的区别在于:
第一种解释:
final关键字定义的常量是在第一次使用时初始化的,初始化后不可改变;const关键字定义的常量在编译时就已经被初始化了,永远不会改变。
第二种解释:
final可以开始不赋值,但是只能赋值一次;而final不仅有const的编译时常量特性,最重要的是它是运行时的常量,并且final是惰性初始化,即在运行时第一次使用前才初始化
举个栗子:
要像下面这样调用一个方法赋给一个常量的时候是不可以的。( 例如下面的:DateTime.now() )
main() {
const PI = 3.141592653; // const定义常量,程序运行之前就知道了值
final PI2 = 3.141592653; // 使用const定义的常量也可以使用final来定义
final nowTime = DateTime.now(); // final定义常量,需要等到程序第一次运行的时候才能知道值
//const nowTime = DateTime.now(); // const是不可以赋值为DateTime.now(),因为编译期DateTime.now()的值不是确定的
}
使用const定义的常量也可以使用final来定义,但是使用 const 定义,这有助于优化性能和节省内存。
另外需要注意的是,常量和变量的类型可以显式指定,也可以通过类型推断来自动推断。例如:
// 使用 const 关键字定义常量,并显式指定类型为 int
const int a = 10;
// 使用 const 关键字定义常量,并自动推断类型为 int
const b = 20;
2.5.3. dynamic
在使用var定义变量的时候,我们无法给一个string类型的变量赋值其他类型数据,但是dynamic类型的变量可以在运行时动态地改变其类型,而不需要在编译时指定类型。
dynamic是一种特殊的类型,它可以表示任何类型的值。
下面是一个使用dynamic类型的简单示例:
dynamic variable = "Hello World";
print(variable); // 输出:Hello World
variable = 42;
print(variable); // 输出:42
variable = true;
print(variable); // 输出:true
在上面的示例中,我们定义了一个名为 variable 的变量,并将其初始化为字符串 "Hello World"。然后,我们将其值更改为整数42,最后再将其值更改为布尔值true。这些更改都是在运行时进行的,而不需要在编译时指定类型。
请注意,由于dynamic类型可以表示任何类型的值,因此它可能会导致一些意想不到的错误,尤其是在代码变得复杂时。因此,建议尽可能使用更具体的类型来声明变量。
2.6. 类型判断与转换
2.6.1. 获取数据类型
通过 变量.runtimeType 可以获取变量当前的类型。
void main() {
String name = "doubi";
print(name.runtimeType); // String
var age = 18;
print(age.runtimeType); // int
dynamic dyn = 1.78;
print(dyn.runtimeType); // double
dyn = "hello";
print(dyn.runtimeType); // String
var person = {
"name": "张三",
"age": 18,
"work": ["李肆", 78],
};
print(person.runtimeType); //_Map<String,Object>
}
2.6.2. 数据类型判断
通过 is 关键字,可以判断当前变量的类型。返回的是一个bool值
import 'dart:ffi';
void main() {
var age = 18;
print(age is int); // true
print(age is String); // false
dynamic dyn = 1.78;
print(dyn is double); // true
print(dyn is String); // false
dyn = "hello";
print(dyn is String); // true
}
2.6.3. int 与 double 转换
int 转换为 double
int i = 3;
double d = i.toDouble();
print(d); // 输出:3
一般好像用不到 int 转 double
double 转 int
小数点之后忽略,向下取整
double d = 3.14;
int i = d.toInt();
print(i); // 输出:3
在取整时,四舍五入
double d = 3.6;
int i = d.round();
print(i); // 输出:4
double d = 3.2;
int i = d.round();
print(i); // 输出:3
2.6.4. String与数字相互转换
数字转换为String
使用 toString() 方法将数字转换为字符串。
int number = 42;
String str = number.toString();
print(str); // 输出:42
String转换为数字
可以使用 parse() 函数将字符串转换为数字。
String str = '42';
int num1 = int.parse(str);
print(num1); // 输出:42
double num2 = double.parse(str);
print(num2); // 输出:42.0
需要注意的是,如果字符串不能被解析为数字,则会抛出一个异常,导致程序终止运行。
因此,建议在使用 parse() 函数时使用 try-catch 语句来处理可能的异常。(异常处理后面再学习)
我们可以使用 tryParse() 函数来将字符串转换为数字,不会抛出异常。
void main() {
var str = 'hello';
var number = int.tryParse(str);
print(number); // 输出:null
}
转换不成功,返回null,不会抛出异常。
需要注意 tryParse(参数) 方法的参数不能为null,转换会报错。
2.6.5. dynamic 转其他类型
将dynamic转换为int、double或String,直接赋值即可。
dynamic variable = 42;
int i = variable;
print(i);
当然类型相同才能复制,否则会报错。
2.7. 命名规则
什么是标识符?
标识符就是名字,例如变量的名字、方法的名字、类的名字。
起名字肯定会有限制,肯定不能 张Three 这样起名字,所以标识符也有限制。
2.7.1. 标识符命名规则
标识符需要遵守一下规则:
- 变量名称必须由数字、字母、下划线和美元符($)组成。
- 注意:标识符开头不能是数字。
- 标识符不能是保留字和关键字。
- 变量的名字是区别大小写的:age和Age是不同的变量,在实际应用中建议不要用一个。
- 标识符(变量名)一定要见名知意:变量名称建议用名词,方法名称建议用动词。
Dart 中有如下关键字:
abstract as assert async await
break case catch class const
continue covariant default deferred do
dynamic else enum export extends
extension external factory false final
finally for Function get hide
if implements import in interface
is late library mixin new
null on operator out part
rethrow return set show static
super switch sync this throw
true try typedef var void
while with yield
2.7.2. 变量命名规范
- 见名知意:看见一个变量名,就知道这个变量名是干嘛的。
a = "zhangsan" // 看到a,鬼知道a是干嘛的
name = "zhangsan" // 看到name,就知道这是个名字,简单明了
personName = "zhangsan" // 在确保明了的前提下,尽量减少长度,这个有点不够简洁
2. 类名使用首字母大写的驼峰规则
class BankAccount {}
class ShoppingCartItem {}
3. 变量使用首字母小写的驼峰规则
String name;
String accountNumber;
2.8. 运算符
下面介绍 Dart 中常见的运算符:算数运算符、赋值运算符、关系运算符。
2.8.1. 算数运算符
算数运算符是干嘛的?
就是进行数学运算的,就是小学学的加、减、乘、除等。
Dart 中常见的算数运算符如下:
| 运算符 | 描述 | 示例 |
|---|---|---|
| + | 加法 | a + b |
| - | 减法 | a - b |
| * | 乘法 | a * b |
| / | 除法 | a / b |
| % | 取余 | a % b |
| ~/ | 整除/取整 | a ~/ b |
举个例子:
main() {
int a = 10;
int b = 3;
print(a + b); // 输出 13
print(a - b); // 输出 7
print(a * b); // 输出 30
print(a / b); // 输出 3.3333333333333335
print(a % b); // 输出 1
print(a ~/ b); // 输出 3
}
2.8.2. 赋值运算符
赋值运算符就是用来给变量赋值的。
2.8.2.1.1. #赋值运算符
| 运算符 | 描述 | 举例 |
|---|---|---|
| = | 右边的值给左边 | a = 123 |
| ??= | 左边为空就把右边给左边 | a??=123 |
int a = 2;
int b = 3;
int c = a + b;
print(c); // 5
var b;
b ??= 123;
print(b); //123
var b = 7;
b ??= 123;
print(b); //7
2.8.2.1.2. 复合赋值运算符
复合赋值运算符就是经过计算后,将值赋给前面的变量。
| 运算符 | 描述 | 举例 |
|---|---|---|
| += | 加法赋值运算符 | c += a 等效于 c = c + a |
| -= | 减法赋值运算符 | c -= a 等效于 c = c - a |
| *= | 乘法赋值运算符 | c *= a 等效于 c = c * a |
| /= | 除法赋值运算符 | c /= a 等效于 c = c / a |
| %= | 取模赋值运算符 | c %= a 等效于 c = c % a |
| ~/= | 整除赋值运算符 | c ~/= a 等效于 c = c ~/ a |
| ??= | 空值赋值运算符 | c ??= a等效于 if(c === null) c=a |
void main() {
int a = 5;
int b = 3;
a += b; // a = a + b
print(a); // 8
a = 5;
a -= b; // a = a - b
print(a); // 2
a = 5;
a *= b; // a = a * b
print(a); // 15
var b;
b ??= 10; // 如果 b 为 null,则将其赋值为 10,如果有值那就不赋值
print(b);//10
}
复合赋值运算符感觉这么写可读性还变差了,有什么好处呢?
- 更加简练;
- 效率更高。
2.8.3. 自增自减运算符
Dart中的自增自减运算符包括++和--。它们可以用于对变量进行加1或减1操作。
void main() {
int a = 5;
a++; // a自增1
print(a); // 输出 6
a--; // a自减1
print(a); // 输出 5
}
需要注意的是,自增自减运算符可以放在变量的前面或后面,这会影响到表达式的值。
如果运算符放在变量的前面,表示先加减,后使用;如果运算符放在变量的后面,表示先试用,后加减。
void main() {
int a = 5;
int b = a++; // 先使用a的值还是5,后加减,a加1,a值为0
print(b); // 5
print(a); // 6
int c = 5;
int d = ++c; // 先加减c+1变为6,后使用,d的值为6
print(d); // 6
print(c); // 6
}
-- 操作符也是一样的。
2.8.4. 关系运算符
关系运算符也就是比较运算符,主要有以下几种:
| 运算符 | < | >= | <= | == | != | |
|---|---|---|---|---|---|---|
| 含义 | 大于 | 小于 | 大于等于 | 小于等于 | 等于 | 不等于 |
void main() {
int a = 10;
int b = 3;
print(a == b); // false
print(a != b); // true
print(a > b); // true
print(a < b); // false
print(a >= b); // true
print(a <= b); // false
String str = '3';
print(str == b); // false,字符串和数字类型是不相等的
}
2.8.5. 逻辑运算符
主要有以下三种:
| 运算符 | && | || | ! | | ---- | ------------------ | --------------------- | ------------- | | 含义 | 与,两边条件同时满足 | 或,两边条件满足一个即可 | 非,取反值,真为假,假为真 | | 逻辑短路 | 第一个值为false则无需计算后面的 | 第一个值为true则无需计算||后面的 | ❌ |
void main() {
bool a = true;
bool b = false;
print(a && b); // false
print(a || b); // true
print(!a); // false
}
可以看到逻辑运算符可以用来判断多个bool类型的条件:
void main() {
int a = 3;
int b = 12;
print(a > 5 && b > 10); // false
print(a > 5 || b > 10); // true
print(!(a > 5)); // true
}
2.8.5.1. 逻辑运算符的优先级
优先级:! > && > ||(类似于先算小括号和乘除再算加减)
void main() {
bool result = !(1 < 2) && 3 == 3 || 1 == 3 && 2 <= 3;
print(result); // 打印:false
}
2.9. 条件判断
2.9.1. if-else
if (condition) {
// condition为true时执行的代码块
} else {
// condition为false时执行的代码块
}
2.9.2. 三目表达式
int max = a > b ? a : b;
2.9.3. if-elseif-else
if (condition1) {
// condition1为true时执行的语句块
} else if (condition2) {
// condition2为true时执行的语句块
} else {
// 所有条件都为false时执行的语句块
}
2.9.4. switch
switch (value) {
case value1:
// value == value1 时执行的代码块
break;
case value2:
// value == value2 时执行的代码块
break;
default:
// value 不等于上面所有的case 时执行的代码块
}
2.10. 循环语句
2.10.1. while
while (condition) {
// condition为真时执行的代码块
}
2.10.2. do-while
do...while循环与while循环的区别在于,do...while循环先执行一次循环体中的代码,再检查条件是否为真。如果条件为真,则继续执行循环体中的代码。
do {
// 循环体中的代码块
} while (condition);
2.10.3. for循环
for (var i = initialValue; i < limit; i += step) {
// 循环体中的代码块
}
2.10.4. 循环嵌套
int day = 1; // 记录第几天
while (day <= 100) { // 外层循环用于循环天数
for (int roseCount = 1; roseCount <= 10; roseCount++) { // 内层循环用于循环花的朵数
print("第${day}天,第${roseCount}朵玫瑰花");
}
day += 1; // 天数+1
}
2.10.5. 中断循环
2.10.5.1. continue 语句
continue的作用是中断本次循环,直接进入下一次循环。
for (int i = 0; i < 5; i++) {
if (i == 3) {
continue;
}
print("i=${i}");
}
执行结果:i = 0 i =1 i=2 i=4
2.10.5.2. break 语句
break 关键字用于直接结束当前所在的循环。
int i = 0;
while (i < 10) {
if (i == 3) {
break;
}
print("i=${i}");
i++;
}
执行结果:i=0 i=1 i=2
2.10.5.3. 死循环
在使用while循环的时候,一定要注意判断条件,否则容易编程死循环。
- 直接设置循环条件为True:
while (true) {
print("死循环");
}
2. 忘记更新判断条件中的变量值,也会变成死循环
int i = 0
while (i < 10) {
print("死循环");
}
for循环也可以写成死循环
for (;;) {
print("死循环");
}
死循环不一定是不对的,有时候我们还需要死循环帮我们做一些事情,要根据实际需求来。
3. 非空安全
null是一个特殊的值,表示一个变量没有被初始化或者是没有值
var a; //只是生命,没有初始化
print(a); //null
非空安全类型是Dart 2.12引入的一项重要功能,以确保不会出现空引用错误。在Dart非空安全系统中,类型被分为可空类型和非空类型。
3.1. 非空类型
默认情况下,所有类型都是非空类型,因此变量、函数参数或返回值不能为null
int age = null;//报错,无法赋空值
String name:
name = null; //报错,无法赋空值
使用非空类型时候,在使用前必须进行赋值
String name;
// print(name); // 报错,使用前必须赋值
name = 'John Doe';
print(name);
3.2. 可空类型
在Dart中,使用?来标记一个变量为可空类型。可空类型标识这个变量可以是一个非空值或者空值
String? name = null;
name = "John Doe";
String firstName = name.split(' ')[0];
print(firstName)
3.3. 类型提升
为了保证安全特性,Dart的流分析已经考虑了空特性。如果一个可空对象不可能有空值,那么就会被当做非空对象处理
String? str;
print(str.length); // str为null 报错
String? str;
if(str != null){
print(str.length); //以确保str不为空,不会编译出错
}
3.4. 非空断言
使用!来进行非空断言。非空断言表示这个变量一定不是空值。
tryParse()方法返回的是可空类型,所以可以使用非空断言,告诉编译器num变量一定不为空,然后将num复制给abc
String str = '123';
int? num = int.tryParse(str);
int abd = num!;
3.5. 空安全调用
使用?.来进行空安全调用。它表示如果这个变量是空值,则不会调用他的函数或者访问它的属性。
String? str = null;
print(str?.length);
3.6. 空值合并运算??
用于判断一个表达式是都为null,如果是null则返回一个默认值,否则返回这个表达式的值
String? name;
String defaultName = "John";
String fullName = name ?? defaultName;
print(fullName); //John
4. 函数
4.1. 定义
函数可以称之为功能,可以将一段实现特定功能的代码封装为一个函数,通过使用函数,提高代码的复用性,减少重复代码,提高开发效率。
- 返回值可以是任何类型或者是
void - 参数可以缺省
return 返回值也可以省略,不返回结果- 函数必须先定义后调用
- 注意:函数内的
return后面如果有语句,是不会被执行的
4.2. 参数
4.2.1. 命名可选参数
使用{}包裹的参数,放在参数列表的末尾
String userInfo = getUserInfo(20);
String userInfo1 = getUserInfo(20, age: 20);
String userInfo2 = getUserInfo(20, age: 20, name: "小红"); //顺序无所谓
String userInfo3 = getUserInfo(20, name: "小红", age: 20);
String userInfo4 = getUserInfo(); //报错
String getUserInfo(int num, {int? age, String? name}) {
return "小明";
}
//------requeired----
void main() {
String userInfo1 = getUserInfo(); //报错
String userInfo3 = getUserInfo(20); //报错
String userInfo4 = getUserInfo(20, age: 20); //报错
String userInfo5 = getUserInfo(20, age: 20, name: "小红");
String userInfo6 = getUserInfo(20, name: "小红", age: 20);
}
String getUserInfo(int num, {int? age, required String name}) {
return "小明";
}
{int? age,String? name}就是可选参数,调用的时候通过名称来传参,顺序无所谓- 在参数前面加个
requeired关键字,在调用的时候就必须传递这个参数
4.2.2. 位置可选参数
使用[]包裹的参数,放在参数列表的末尾
void main() {
String userInfo1 = getUserInfo(); //报错
String userInfo3 = getUserInfo(20);
String userInfo4 = getUserInfo(20, 20);
String userInfo5 = getUserInfo(20, "小红"); //报错
String userInfo6 = getUserInfo(20, null, "小明");
}
String getUserInfo(int num, [int? age, String? name]) {
return "小明";
}
[int? age,String? name]就是位置可选参数,- 调用时,如果给一个位置可选参数传递值,那在它前面的参数都要传递值,不能省略,哪怕传递
null
4.2.3. 默认参数
void main() {
getUserInfo(20);
}
//命名可选参数
void getUserInfo(int num, {int age = 50, String name = "小红"}) {
print(age); //50
print(name); //小红
}
//位置可选参数
void getUserInfo(int num, [int age = 50, String name = "小红"]) {
print(age); //50
print(name); //小红
}
4.2.4. 参数类型
- 上面的函数参数都是指定类型的,参数可以不指定类型
- 如果不指定的话,函数的类型默认是动态的(自动推断为
dynamic) - 可以接受不同类型的值,灵活性较强,一些情况下是可用的,但是会导致类型安全方面的隐患,特别是函数内部可能会因类型不匹配而导致运行时错误(就像下面这样)
int add(a, b) {
return a + b;
}
void main() {
add(true, false); //报错
}
4.2.5. 函数重载
方法名一样,参数不同
在dart中,不支持函数的重载
4.3. 返回值
- 可以在函数生命的时候指定,也可以根据函数体重的代码自动推断出来
- 函数没有指定返回值,则默认返回
null
subtract(int a, int b) {
// 没有返回值类型,会自动推断出返回值类型
return a - b;
}
int add(int a,int b){
return a + b;
}
4.4. 函数嵌套调用
- 一个函数可以调用另外一个函数,另一个函数还可以继续调用其他函数
void funB() {
print("----b");
}
void funA() {
print("----a1");
funB();
print("----a2");
}
void main() {
funA();
}
4.4.1. 闭包
- 全局变量特点: 全局变量常驻内存、全局变量污染全局
- 局部变量的特点: 不常驻内存会被垃圾机制回收、不会污染全局
想实现的功能:①常驻内存②不污染全局;产生了闭包,闭包可以解决这个问题.....
闭包: 函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)
闭包的写法: 函数嵌套函数,并return 里面的函数,这样就形成了闭包。
fn() {
var a = 123; /*不会污染全局 常驻内存*/
return () {
a++;
print(a);
};
}
var b = fn();
b(); //124
b(); //125
b(); //126
4.5. 变量作用域
局部变量 & 全局变量
4.5.1. 局部变量
- 在函数内部定义的变量,这种变量只能在变量内部使用
- 局部变量当函数调用完成,就被销毁释放了。
void funA() {
int num = 3;
print(num);
}
void main() {
// print(num); // 无法访问到funA()内定义的变量num
}
4.5.2. 全局变量
在函数内、外都可以调用的变量
int a = 1; // 定于全局变量a
int b = 2; // 定于全局变量b
void main() {
int a = 5;
{
int a = 10;
print(a); // 优先在所在的{}内查找变量
}
print(a); // 优先在所在的{}内查找变量,即main()函数内查找a
print(b); // 优先在所在的{}内查找变量,main函数内没找到,则继续向外寻找
}
4.6. 函数递归
在一个函数内部,又调用了这个函数自己,这个就是函数的递归。
void funA() {
print("----");
funA();
}
void main() {
funA();
}
上面的函数,执行 funA(),然后在 funA() 内部又调用了自己,那么会重新又调用了 funA() 函数,然后又调用了自己,这样就会一直无限调用,变成了无限循环调用,执行的时候很快就报Stack Overflow的错误,栈溢出。
所以函数的递归有时候是很危险的,很容易无限调用,造成栈溢出,程序崩溃。所以函数的递归调用一定要注意结束或跳出递归的条件。
4.7. 匿名函数 & 箭头函数
匿名函数就是一种没有显示命名函数,一般顶一万会将函数赋值给一个变量,可以作为函数的参数使用
var add = (int a, int b) {
return a + b;
};
print(add(3, 5)); // 8
如果匿名函数只有一个表达式,可以使用箭头 => 语法:
var multiply = (int a, int b) => a * b;
4.8. 闭包
闭包(Closure) 是一个可以访问其作用域外变量的函数。闭包不仅可以访问自身作用域内的变量,还能访问外部作用域的变量,即使外部作用域的变量已经超出了其正常的生命周期,闭包仍然能够保持对这些变量的引用。
4.8.1. 基本用法
void main() {
Function makeAdder(int addBy) {
return (int number) => number + addBy;
}
var add5 = makeAdder(5); // 创建一个闭包,addBy = 5
var add10 = makeAdder(10); // 创建一个闭包,addBy = 10
print(add5(3)); // 输出 8
print(add10(3)); // 输出 13
}
makeAdder返回了一个匿名函数(int number) => number + addBy。add5和add10都是闭包,它们分别持有addBy变量的不同值(5 和 10)。- 这些闭包在
makeAdder执行完毕后仍然可以访问addBy,即使makeAdder已经返回了。
4.8.2. 闭包捕获外部变量
void main() {
var counter = 0;
Function incrementCounter = () {
counter++;
print(counter);
};
incrementCounter(); // 输出 1
incrementCounter(); // 输出 2
incrementCounter(); // 输出 3
}
incrementCounter访问了counter变量,而counter定义在main作用域中。- 即使
main执行完毕,incrementCounter依然可以访问并修改counter。
4.8.3. 闭包在Dart中的应用
- 回调函数(Callback)
- 函数式编程(Functional Programming)
- 事件监听(Event Listeners)
- 数据缓存(Data Caching
4.8.4. 总结
- 闭包可以访问外部作用域的变量,即使外部作用域已经结束。
- 闭包捕获的是变量的引用,而不是值,可能会导致意外的行为(如
for循环的例子)。 - 可以在循环中创建新的局部变量来解决引用问题。
- Dart 的闭包非常强大,在实际开发中(比如
Flutter事件处理、状态管理等)经常会用到它们。
5. 面向对象
5.1. 类和对象
5.1.1. 类的定义和使用
类:可以定义属性和行为
属性就是变量,表示这个类有哪些数据信息
行为就是方法,表示这个类能干什么
/**
* 定义类
*/
class Student {
String sid = "001";
String name = "Doubi";
int age = 12;
void study() {
print("我是${name}, 我$age岁了, 我在学习");
}
}
/**
使用类
*/
void main() {
Student stu1 = new Student();
print(stu1.name); // 访问成员变量
print(stu1.age);
stu1.study();
Student stu2 = Student();
stu2.name = "ShaBi";
stu2.age = 13;
stu2.study();
}
- 通过
new 类名()可以创建一个对象,new可以省略。 - 创建对象后,我们可以通过
对象.属性来访问变量,也可以通过对象.属性 = 值来给属性赋值。 - 使用
对象.方法()可以调用方法。
5.1.2. 默认构造函数
- 在创建类的时候,是给属性直接赋值
String name = "Doubi";但是在实际的应用中,一般是使用不同的属性值创建不同的对象,而且可以在创建对象的时候对属性进行初始化,这里就需要用到构造函数。 - 构造函数的名称和类名是一致的
late:为了可以在构造函数中进行初始化,给类中的属性添加一个late关键字,表示延迟初始化(或者定义为可空类型)。
class Student {
late String sid;
String? name;
late int age;
// 构造函数
Student(String sid, String name, int age) {
this.sid = sid; // this.sid访问的是属性,sid访问的是形参
this.name = name;
this.age = age;
}
void study() {
print("我是${name}, 我$age岁了, 我在学习");
}
}
定义为String/int都是非空类型(sid 和 age),所以定义的时候必须要初始化,否则报错,所以根据前面的说明,前面要加个late
- 不写构造函数的时候,类是有一个无参的隐式构造函数的。如果写了显式的构造函数,则饮食的构造函数就会失效,如下:
class Student {
late String sid;
String? name;
late int age;
// 无参构造函数
Student() {}
}
5.1.3. this的作用和构造函数的简写
我们写构造函数的时候,形参和类的属性吗相同(当然也能不同),导致在构造函数中,不知道到底调用的是哪一个,使用this,表示访问的是类的属性
/**
* 定义类
*/
class Student {
late String sid;
String? name;
late int age;
// 构造函数
Student(String sid, String name, int age) {
this.sid = sid;
this.name = name;
this.age = age;
}
void study() {
print("我是${this.name}, 我${this.age}岁了, 我在学习");
}
}
void main() {
Student stu1 = new Student("001", "张三", 12);
stu1.study();
Student stu2 = Student("002", "李四", 13);
stu2.study();
}
当使用stu1调用study()方法的时候,this指的就是张三这个对象,那么this.name的值就是张三;当我们使用stu2调用study()方法的时候,this指的就是李四这个对象,那么this.name的值就是李四
🎈构造函数简写形式
class Student(){
late String name;
String? sex;
late int age;
Student(this.name,this.age,this.sex);
}
5.1.4. 命名构造函数
dart不能像java中一样进行构造函数的重载,如果要实现不同的参数创建对象,需要使用命名构造函数
重载:方法名一样,参数个数和类型不同
class Student {
late String sid;
String? name;
late int age;
// 默认构造方法
Student(this.sid, this.name, this.age);
// 定义命名构造函数,类名后的名称是自定义的
Student.initByNameAndAge(this.name, this.age);
// 新的构造方法
Student.fromMap(Map<String, Object> map) {
this.name = map['name'] as String;
this.age = map['age'] as int;
}
}
void main() {
// 通过默认构造函数创建对象
Student stu1 = Student("001", "Doubi", 12);
stu1.study();
// 通过命名构造函数创建对象
Student stu2 = Student.initByNameAndAge("ShaBi", 13);
stu2.sid = "002";
stu2.study();
// 通过命名构造函数创建对象
Student stu3 = Student.fromMap({"name": "ErBi", "age": 14});
stu3.study();
}
- 定义和使用命名构造函数时,需要在类名后面加上构造函数的名称,名称是自己定义idea,但是名称必须和类名不同,否则会被解析为默认构造函数。
- 通过命名构造函数就可以通过不同的参数来创建对象了。
5.1.5. 初始化列表
- 初始化列表会在构造函数方法体执行之前执行,多个初始化列表表达式之间使用,分割。
- 初始化列表常用于设置
final修饰的属性的值,也可以使用assert进行判断参数,进行形参的校验。
class Person {
String name;
int age;
Person(String name, int age)
: name = name.toUpperCase(), // 使用表达式计算初始值
age = age + 1 {
// 初始化列表会在构造函数体之前执行
print("name:${this.name}, age:${this.age}");
}
Person.fromJson(Map<String, dynamic> json)
: name = json['name'],
age = json['age']; // 使用初始化列表初始化成员变量
}
void main() {
Person person = Person("Doubi", 12);
}
- 会先执行构造函数括号后面的初始化列表表达式
- 表达式可以是多个语句,使用,分隔,执行完初始化列表之后才执行后早函数的函数
-
- 初始化name
- 使用name.toUpperCase()将name转换成大写字母
- 初始化age
- 使用age + 1改变age的值
在Dart中,final修饰的属性只能在声明时或者在构造函数的初始化列表中进行赋值。一旦final属性被赋值,就不能再被修改。所以我们可以使用初始化列表final修饰的属性进行赋值。
class Person{
final String name;
final int age;
Person(String name,int age) : name = name.toUpperCase()
}
除了初始化成员变量,初始化列表还可以对构造函数的参数进行校验
class Person {
final String name;
final int age;
Person(String name,int age) :
assert(age >= 0),
name = name.toUpperCase(),
age = age + 1;
}
assert:断言
- 使用
assert语言对构造函数的age参数进行校验。如果age小于0,则会抛出一个异常。 assert(age >= 0)就是肯定age >= 0一定是对的,否则程序报错,终止执行。
注意:
assert只在开发模式生效,在生产环境会被自动忽略。因此,它主要用于开发和调试阶段,不应该依赖于assert语句来处理正常的错误情况- 在
vs code上运行的时候,assert不生效,可以运行指定参数或者使用命令行运行
dart --enable-asserts Main.dart
运行会报错,提示:Failed assertion: line 6 pos 16: 'age >= 0': is not true.
5.1.6. 重定向构造方法
也就是一个构造方法调用另外一个构造方法
Person(this.name, this.age);
Person.fromName(String name) : this(name, 0);
Person.defaultValue() : this.fromName("小红")
注意:是在冒号后面使用this调用。
5.1.7. 常量构造方法
常量构造方法:加const修饰,可以保证相同的参数,创建出来的对象是相同的
默认情况下,创建对象的时候,即使传入相同的参数,创建出来的也不是一个对象
class Person {
String name;
int age;
Person(this.name, this.age);
}
main(List<String> args) {
var p1 = Person('逗比', 12);
var p2 = Person('逗比', 12);
print(identical(p1, p2)); // 输出: false
}
identical(对象1, 对象2):基于对象的内存地址来判断两个对象是否是一个对象
class Person {
final String name;
final int age;
const Person(this.name, this.age);
}
main(List<String> args) {
var p1 = const Person('逗比', 12);
var p2 = const Person('逗比', 12);
const p3 = Person('逗比', 12);
print(identical(p1, p2)); // 输出: true
print(identical(p2, p3)); // 输出: true
}
注意:
- 用于常量构造方法的类中,所有的成员变量必须是
final修饰的 - 为了可以通过常量构造方法,创建出相同的对象,不再使用
new关键字,而是使用const关键字,注意const不能省略,否则创建的是不同的对象,除非是将结果赋值给const修饰的标识符时,const可以省略 - 多个对象的属性值必须是一样的,创建出来的对象才能是一个对象。
在flutter中经常用到常量构造函数,const不仅仅节约创建组件的内存开销,还可以在重新构建组件的时候,不重新构建const组件,从而提高性能。
5.1.8. 私有构造函数
- 成员变量和方法前面添加下划线
_----> 变量和方法声明为私有的 - 构造函数前面添加下划线
_------> 构造函数声明为私有的 - 只能在类的内部访问,外部无法直接实例化
class Person {
String name;
int age;
// 私有构造函数
Person._(this.name, this.age);
// 私有命名构造函数
Person._initByNameAndAge(this.name, this.age);
}
注意:只有在文件内部调用才算是私有的,在当前文件都是可以调用的
问题:构造函数为什么要私有化呢,如果构造函数私有化不就无法创建对象了?
一般情况下我们不会将命名构造函数私有化,这个真没什么必要,但是有时候我们需要将默认构造函数私有化,来禁止外部通过默认构造函数创建对象,想创建对象必须通过指定的命名构造函数或工厂构造函数来创建对象。
5.1.9. 工厂构造函数
定义:一种特殊类型的构造函数,用于创建对象。与普通构造函数不同,工厂构造函数可以返回一个已存在的对象或者通过其他方式创建对象。
class Person {
late String name;
late int age;
Person(this.name, this.age);
// 命名构造函数
Person.fromJson_1(Map<String, dynamic> json) {
this.name = json['name'];
this.age = json['age'];
}
// 工厂构造函数
factory Person.fromJson_2(Map<String, dynamic> json) {
return Person(json['name'], json['age']);
}
void displayInfo() {
print('Name: $name, Age: $age');
}
}
void main() {
Map<String, dynamic> json = {'name': 'Alice', 'age': 25};
Person person1 = Person.fromJson_1(json);
person1.displayInfo();
// 使用工厂构造函数创建对象
Person person2 = Person.fromJson_2(json);
person2.displayInfo();
}
注意:
- 使用
factory关键字声明,后跟构造函数名称。 - 命名构造函数用于通过不同的方式或参数来实例化对象,但始终返回当前类的实例;工厂构造函数可以返回一个已存在的对象实例,也可以返回其他类的实例,甚至是
null - 命名构造函数通常用于为类提供多种不同的实例化方式,以满足不同的需求;工厂构造函数常用于实现复杂的对象创建逻辑,例如对象池、缓存、单例模式等。
- 命名构造函数通过使用类名和构造函数名称的组合调用,可以使用
new来创建;工厂构造函数通过类名直接调用,类似于静态方法的调用方式。
5.1.9.1. 单例模式
一种设计模式,可确保一个类中只有一个实例对象,并提供一个获取该实例的方法。也就是说通过一个类只能创建一个实例对象
class Singleton {
static late final Singleton _instance = Singleton._();
// 私有的构造函数
Singleton._();
factory Singleton() {
return _instance;
}
void displayMessage() {
print('This is a singleton instance.');
}
}
void main() {
Singleton instance1 = Singleton();
Singleton instance2 = Singleton();
print(identical(instance1, instance2)); // 输出:true
}
- 首先将构造函数私有化
Singleton._();,这样外部的代码就无法调用类的构造函数创建对象了。 - 然后提供一个工厂构造函数给外部调用,外部只能通过该工厂构造函数获取当前类的对象
_instance。 - 当前类的对象
_instance在类第一次执行的时候的时候就创建了,而且是一个静态的对象,只有一份。 - 所以每次通过工厂构造函数获取到的类实例,都是同一个。
5.1.10. 静态变量
- 上面定义的属性和方法,是实例变量和实例方法,也叫成员变量和成员方法。
- 实例变量对每个实例来说,是独立的数据,每个对象不互相影响。每个对象都会开辟出独立的内存空间存储对象的实例变量数据。
- 但是无论多少对象,实例方法都只有一份,所有对象共享,通过
this,来确定是哪个对象调用了实例方法
在类中还可以定义各个对象共享的数据,也就是静态变量。
🌰 定义了一个Student类,然后通过Student类来创建对象,我们想知道一共创建了多少个Student对象,应该如何操作呢
/**
* 定义类
*/
class Student {
static int stuCount = 0; // 定义一个静态变量,用于记录创建的对象个数
late String sid;
String? name;
late int age;
// 构造函数
Student(String sid, String name, int age) {
stuCount++; // 创建对象会调用构造函数,调用一次就+1
//Student.stuCount++;
this.sid = sid;
this.name = name;
this.age = age;
}
}
void main() {
Student stu1 = Student("001", "Doubi", 12);
Student stu2 = Student("002", "Shabi", 13);
Student stu3 = Student("002", "ErBi", 14);
print(Student.stuCount); // 输出: 3
}
定义了一个类,然后在类中定义了一个 stuCount 静态变量。
当创建对象的时候,会调用构造函数,我们在构造函数中将stuCount++,这样就可以记录调用构造函数的次数
静态变量是属于类的,而不是属于类的实例。静态变量可以通过 类名.静态变量 来赋值和访问。
5.1.11. 静态方法
除了静态变量,还有静态方法。静态变量也是属于类的,而不是属于类的实例。
/**
* 定义类
*/
class Student {
static int stuCount = 0;
late String sid;
String? name;
late int age;
// 构造函数
Student(String sid, String name, int age) {
Student.stuCount++;
this.sid = sid; // this.sid访问的是属性,sid访问的是形参
this.name = name;
this.age = age;
}
// 静态方法
static void getStuCount() {
print("一共创建了${Student.stuCount}个学生");
}
}
void main() {
Student("001", "Doubi", 12);
Student("002", "Shabi", 13);
Student("002", "ErBi", 14);
Student.getStuCount(); // 输出: 一共创建了3个学生
}
注意:
- 静态方法不能访问非静态变量。静态方法是通过
类.静态方法()调用的,不是通过对象的实例调用的,而非静态变量是属于类的实例的,所以如果类.静态方法()中调用的非静态变量没法确定是哪个实例 - 在非静态方法中可以访问静态变量,也是通过
类名.静态变量来赋值和访问。 - 静态方法一般用来定义一些工具类。
5.1.12. 内置方法
toString() 方法是 Dart 中的一个内置方法,用于返回表示对象的字符串表示形式,当我们打印对象的时候,就会将对象转换为字符串进行输出,则会调用toString() 方法。默认情况下,toString() 方法会返回对象的类型。
因为所有的类都是继承自Object类的,所以我们在类中不写 toString() 方法,默认是调用了Object类的toString() 方法。
class Person {
String name;
int age;
Person(this.name, this.age);
}
void main() {
var person = Person('John', 25);
print(person); // 输出:Instance of 'Person'
}
因此:通常建议在自定义类中重写 toString()方法,以便更好地控制对象的字符串表示形式。
class Person {
String name;
int age;
Person(this.name, this.age);
@override
String toString() { // 重写toString()方法
return 'Person{name: $name, age: $age}';
}
}
void main() {
var person = Person('DouBi', 12);
print(person); // 输出:Person{name: John, age: 25}
}
打印对象,会调用 toString() 方法,打印 toString() 方法返回的内容。
方法上的 @override 是一个注解,表示这个方法是重写父类的方法,可以省略,但建议加上。
5.1.13. 级联操作
允许你在一个对象上连续调用多个方法或访问多个属性,而无需重复引用该对象,这种技术可以使代码更简洁、易读,并且可以在单个表达式中对同一个对象进行多个操作。
级联操作使用连续的 .. 运算符来实现
var person = Person()
..setName('John')
..setAge(30)
..printInfo();
5.2. 封装
定义:指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问
一个事物会有很多属性和方法,但是并不是多有的属性和方法都需要开放出来
例如我们定义了一个手机类,我们可以使用手机打电话、拍照等,但是我们并不关心手机电压,驱动信息,也不关心内存的分配,CPU的调度等,虽然这些都属于手机的属性和行为。
我们可以将用户不关心的属性和方法封装并隐藏起来,只给类内部的方法调用,例如上网会用到4G模块,但是不是由用户来使用4G模块,而是由手机上网的功能开调用4G模块,只开放用户直接使用的信息和功能
5.2.1. 私有属性和方法
基于文件的!!基于文件的!!基于文件的!!
将不想暴露出去的变量和方法隐藏起来,这时候就需要用到私有成员变量和私有成员方法。
定义方式:
- 定义私有成员变量:变量名以
_开头,一个下划线开头。 - 定义私有成员方法:方法名以
_开头,一个下划线开头。
class Phone {
late String producer;
late int _voltage;
Phone() {
this.producer = "华为"; // 手机品牌
this._voltage = 12; // 电压
}
void call() {
print("打电话");
print("手机品牌:${this.producer}");
print("手机电压:${this._voltage}");
}
// 定义一个私有方法
void _get_run_voltage() {
print("当前电压:${this._voltage}");
}
}
void main() {
Phone phone = Phone();
phone.call();
phone.producer = "小米";
phone._voltage = 24;
phone.call();
phone._get_run_voltage();
}
问题:理论上私有属性和私有方法只能在类内部的方法中调用,不能通过对象来调用,那为什么可以在main函数中使用phone._voltage、phone._get_run_voltage()呢????
回答:因为main函数和Phone类是写在一个文件中的, 如果要实现私有属性和方法,需要将定义和调用写在不同的文件中 。
import 'domain/Phone.dart';
void main() {
Phone phone = Phone();
phone.call();
phone.producer = "小米";
// phone._voltage = 24; // 报错,无法访问私有变量
phone.call();
//phone._get_run_voltage(); // 报错,无法访问私有方法
}
如果不想暴露的属性和方法可以定义为私有成员。私有属性和私有方法只能在类内部的方法中调用,不能通过对象来调用。
5.2.2. getter和setter
前面有写,封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。
要访问内部私有的属性,提供了两个方法,getter获取私有属性,setter设置私有属性值。
class Phone {
late String _producer;
late int _voltage;
Phone(this._producer, this._voltage);
get producer { // 定义producer的getter方法,用于获取_producer私有属性
return this._producer;
}
set producer(producer) { // 定义producer的setter方法,用于给_producer私有属性赋值
this._producer = producer;
}
get voltage {
return this._voltage;
}
set voltage(voltage) {
if (voltage <= 36) {
this._voltage = voltage;
} else {
throw Exception("参数错误"); // 限制传入的参数值,进行报错处理,异常处理后面再学习
}
}
}
通过 get和set 关键字定义getter和setter,用于访问私有属性和给私有属性赋值。
import 'domain/Phone.dart';
void main() {
Phone phone = Phone("华为", 12);
print(phone.producer); // 通过getter方法获取变量,并不是直接访问变量
print(phone.voltage);
phone.producer = "小米"; // 通过setter方法设置变量,并不是直接给变量赋值
phone.voltage = 24;
print(phone.producer);
print(phone.voltage);
}
需要注意上面 phone.producer 和 phone.voltage 是通过属性的方式调用,但是实际上是调用了getter和setter,并不是直接访问变量和给变量赋值。
这样做的话,严格控制了属性获取和设置的入口,如果通过 对象.属性 来修改,代码很多的时候完全会不知道在哪里修改了属性导致出现了问题。另外我们可以在setter方法中对属性的设置进行限制。
5.3. 继承
class 子类名 extends 父类名 {
类内容
}
注意
- 继承不能继承父类私有的成员变量和方法。
- 如果父类的成员变量和属性不想被子类继承,可以设置为私有成员。
- 一个类如果没有写明继承哪个类,那么默认都是集成自Object类,所以最终的结果就是Object类是所有类的父类!
5.3.1. 重写父类的方法
继承父类就拥有了父类的属性和方法(私有属性和方法除外)。如果父类的属性和方法不是想要的,还能进行覆盖。
class Bird {
// 定义一个鸟类
late int age; // 鸟都有年龄
void fly() {
// 定义了一个飞的方法
print("我${this.age}岁了,我会飞");
}
void tweet() {
// 定义了一个叫的方法
print("我会叫");
}
}
class Sparrow extends Bird {
// 定义一个麻雀类,继承自鸟类
void tweet() {
print("我会啾啾叫");
}
}
class Pigeon extends Bird {
// 定义一个鸽子类,继承自鸟类
}
void main() {
Sparrow sparrow = Sparrow(); // 创建一个麻雀对象
sparrow.age = 1;
sparrow.fly();
sparrow.tweet();
Pigeon pigeon = Pigeon(); // 创建一个鸽子对象
pigeon.age = 2;
pigeon.fly();
pigeon.tweet();
}
问题:如果我在父类的fly()方法中调用tweet()方法,然后子类方法调用fly()方法,那么会如何执行呢?
sparrow.fly() 调用的是继承自父类的方法,在 fly() 方法中调用了 tweet() 方法,那么调用的是父类的tweet() 方法还是子类的tweet() 方法呢?调用子类的方法
class Bird {
void fly() {
// 定义了一个飞的方法
print("我会飞");
tweet();
}
void tweet() {
// 定义了一个叫的方法
print("我会叫");
}
}
class Sparrow extends Bird {
void tweet() {
print("我会啾啾叫");
}
}
void main() {
Sparrow sparrow = Sparrow(); // 创建一个麻雀对象
sparrow.fly(); //我会飞 我会啾啾叫
}
5.3.2. 调用父类的方法
如果我们想要在子类中调用父类被重写的属性和方法,使用 super 关键字来调用父类的属性或方法。
lass Bird {
// 定义一个鸟类
int age = 1; // 鸟都有年龄
void fly() {
// 定义了一个飞的方法
print("我${this.age}岁了,我会飞");
}
void tweet() {
// 定义了一个叫的方法
print("我会叫");
}
}
class Pigeon extends Bird {
late int age;
// 定义一个鸽子类,继承自鸟类
void tweet() {
// 重写了父类叫的方法
print("我会咕咕叫");
}
void test() {
print("age:${super.age}"); // 调用父类的属性
super.tweet(); // 调用父类的方法
print("age:${this.age}");
this.tweet();
}
}
void main() {
Pigeon pigeon = Pigeon(); // 创建一个鸽子对象
pigeon.age = 10;
pigeon.test(); //age:1 我会叫 age:10 我会咕咕叫
}
如果要在子类中调用父类的属性 ,可以通过 super.父类属性 来调用,同样,如果要在子类中调用父类的方法,可以通过 super.父类方法() 来调用。
5.3.3. 构造函数的继承
子类继承父类,是没办法直接继承构造函数的
但是子类的构造函数是会隐式的调用无参的构造函数
class Bird {
late int age;
Bird() {
print("执行了 Bird 构造函数");
}
}
class Pigeon extends Bird {
Pigeon() {
print("执行了 Pigeon 构造函数");
}
Pigeon.fromName(String name) {
print("执行了 fromName 明明构造函数");
}
}
void main() {
Pigeon(); // 创建对象,没有传入参数
Pigeon.fromName("DouBi");
}
执行结果:
执行了 Bird 构造函数
执行了 Pigeon 构造函数
执行了 Bird 构造函数
执行了 fromName 明明构造函数
通过结果可以看到,子类的构造函数会先调用父类的无参构造函数 。
如果父类定义了有参的构造函数,那么就没有无参的构造函数了,那么就会报错。
🌰
class Bird {
late int age;
Bird(this.age); // 定义了有参的构造函数
}
class Pigeon extends Bird {} //报错
因为Pigeon类没有写构造函数,使用的是无参构造函数,执行的时候会调用父类Bird的无参构造函数,但是Bird类没有无参构造函数,所以会报错。
所以在写Pigeon类的构造函数的时候,要显式的调用父类的构造函数。
调用父类的构造函数需要在构造函数的初始化列表中使用super()来调用:
lass Bird {
late int age;
Bird(this.age); // 定义了有参的构造函数
}
class Pigeon extends Bird {
Pigeon(int age) : super(age); // 使用super调用父类的构造函数
}
class Bird {
late int age;
Bird(this.age);
Bird.fromMap(Map<String, Object> map) : this.age = map['age'] as int;
}
class Pigeon extends Bird {
Pigeon(int age) : super(age); // 使用super调用父类的构造函数
// 调用父类的命名构造函数
Pigeon.fromMap(Map<String, Object> map) :
super.fromMap(map) {
}
}
5.3.4. 类型判断
5.3.4.1. runtimeType
runtimeType属性:判断某个对象是否是某个类型
注意:子类的对象不是父类的对象的类型。
void main() {
Pigeon pigeon = Pigeon();
Sparrow sparrow = Sparrow();
print(pigeon.runtimeType); // 输出: Pigeon
print(sparrow.runtimeType); // 输出: Sparrow
print(pigeon.runtimeType == Pigeon); // true,鸽子对象的类型是鸽子
print(pigeon.runtimeType == Bird); // false,鸽子对象的类型不是鸟
print(sparrow.runtimeType == Sparrow); // true,麻雀对象的类型是麻雀
}
5.3.4.2. is
is :判断某个对象是否是某个类型的实例,子类对象也是父类对象的实例。
void main() {
Pigeon pigeon = Pigeon();
print(pigeon is Pigeon); // true,鸽子对象是鸽子类型的实例
print(pigeon is Bird); // true,鸽子对象是鸟类的实例
print(pigeon is Sparrow); // false,鸽子对象不是麻雀类的实例
}
5.3.5. Mixins
在C++、Python中是支持多继承的,但是在Dart中是不支持多继承的。
在Dart中,新增了一种复用代码的机制Mixins,中文意思是混入,允许你在类中重用其他类的代码,而无需继承这些类。它提供了一种灵活的方式来组合和共享功能,以实现代码重用和组合。
在Dart中,有两种方式可以定义Mixin:使用关键字mixin定义Mixin和使用 class 定义一个类。
🌰
class Bird {
// 定义一个鸟类
late int age; // 鸟都有年龄
void fly() {
// 定义了一个飞的方法
print("我${this.age}岁了,我会飞");
}
}
class Poultry {
// 定义一个家禽类
late int number; // 家禽需要编号
void eat() {
// 定义了一个吃的方法
print("我吃饭啦");
}
}
class Chicken extends Bird with Poultry {
// 定义一个鸡类,继承自鸟类和家禽类
void fly() {
print("我不会飞");
}
}
void main() {
Chicken chicken = Chicken(); // 创建一个鸡对象
chicken.age = 1;
chicken.number = 9527;
chicken.fly();
chicken.eat();
}
5.4. 多态
5.4.1. 什么是多态
定义:多种状态,同一个类型的父类型对象,因为指向的是不同的子对象,而表现出的不同的状态。多态是建立在继承的基础之上的
作用:
- 提高代码的维护性
- 提高代码的扩展性
- 把不同的子类对象当做父类来看待,可以屏蔽不同子类对象之间的差异,写出通用的代码,以适应需求的不断变化。
class Bird {
void tweet() {}
}
class Sparrow extends Bird {
void tweet() {
print("我会啾啾叫");
}
}
class Pigeon extends Bird {
void tweet() {
print("我会咕咕叫");
}
}
void main() {
Bird bird1 = Sparrow(); // 创建一个麻雀对象
Bird bird2 = Pigeon(); // 创建一个鸽子对象
bird1.tweet(); //我会啾啾叫
bird2.tweet(); //我会咕咕叫
}
5.4.2. 多态的使用
实现一个功能:学生做交通工具去某个地方,交通工具可能是汽车、飞机。
5.4.2.1. 原始写法
class Car {
void run(String destination) {
print("开车去->${destination}");
}
}
class Plane {
void fly(String destination) {
print("飞去->${destination}");
}
}
class Student {
void go_to(var vehicle, String destination) { // 传入交通工具
if (vehicle.runtimeType == Car) { // 判断交通工具的类型,然后调用交通工具的方法
vehicle.run(destination);
} else if (vehicle.runtimeType == Plane) {
vehicle.fly(destination);
}
}
}
//调用代码
void main(List<String> args) {
Student stu = Student();
Car car = Car();
Plane plane = Plane();
stu.go_to(car, "北京");
stu.go_to(plane, "新疆");
}
缺点:
- 不易于扩展,如果我们现在增加一个交通工具火车,则还需要修改Student类的go_to()方法,针对新的交通工具来处理,因为学生类和汽车、飞机类直接存在依赖关系,耦合性高。
- 违反了设计原则中的开闭原则,对扩展是开放的,对修改是封闭的,也就是允许在不改变它代码的前提下变更它的行为。
5.4.2.2. 多态写法
class Vehicle {
void transport(String destination) {}
}
class Car extends Vehicle {
void transport(String destination) {
print("开车去->${destination}");
}
}
class Plane extends Vehicle {
void transport(String destination) {
print("飞去->${destination}");
}
}
class Student {
void go_to(Vehicle vehicle, String destination) {
vehicle.transport(destination);
}
}
//调用代码
void main(List<String> args) {
Student stu = Student();
Car car = Car();
Plane plane = Plane();
stu.go_to(car, "北京");
stu.go_to(plane, "新疆");
}
上面的代码使用了多态,学生类与各个交通工具子类已经不直接产生关系,遵从了设置原则中的依赖倒置原则(程序依赖于抽象接口,不要依赖于具体实现)。
5.4.3. 抽象类
含有抽象方法的类就是抽象类
抽象方法:没有方法体,方法体为空的方法
abstract class Vehicle {
void transport(destination);
}
抽象类中也可以包含非抽象方法,子类继承抽象类需要实现(重写)抽象类中所有的抽象方法。
抽象类的作用:
抽象类是不能被实例化的,也就是不能创建对象,一般抽象类都是作为父类使用,父类用来确定用哪些方法,相当于用来确定设计的标准,用于对子类做出约束
子类用来实现父类的标准,负责具体的视线,配合多态使用,获得不同的工作状态
5.4.4. 接口
如果一个抽象类中的方法都是抽象方法,我们可以将这个抽象类定义成接口
java中,使用interface关键字,和定义抽象类是不同的。
dart中,没有专门的关键字定义接口
dart的接口就是通过类来实现的,所以一个抽象类是接口还是抽象要开怎么使用
通过extends来使用就是抽象类,通过implements来实现就是接口
接口中只能定义常量和抽象方法,所以抽象类的作用主要是提供约束和规范。
// 定义一个抽象类
abstract class Bird {
void tweet();
}
// 定义一个接口
abstract class Animal {
void breathe();
}
// 定义一个接口
abstract class Helpful {
void help();
}
class Sparrow extends Bird implements Animal, Helpful {
@override
void breathe() {
print("我要呼吸");
}
@override
void tweet() {
print("我会啾啾叫");
}
@override
void help() {
print("有益的");
}
}
void main() {
Animal animal = Sparrow();
animal.breathe();
}
一个类只能有一个父类,但是可以有多个接口。一个类需要实现抽象类和接口中所有的抽象方法,否则编译会报错。
5.4.5. 抽象类和接口的区别
1️⃣ 定义方式
- 抽象类只用
abstract class关键字定义 - 接口没有单独的关键字,默认情况下,所有的类都可以作为接口实现(通过
implements关键字)
2️⃣ 继承和实现方式
- 抽象类可以被继承,子类可以继承其方法和属性,并可以重写抽象方法
- 接口是通过
implements实现的,必须重写所有方法,不能继承实现
3️⃣ 方法和属性
- 抽象类可以包含已实现的方法(可以有方法体),也可以有抽象方法(没有方法体)
- 接口本质上是一个约定,所有的方法都是未实现的,不能包含已实现的方法(除非是default方法,不过目前Dart不支持)
4️⃣ 构造函数
- 抽象类可以有构造函数
- 接口不能有构造函数(因为本身不能进行实例化)
5️⃣ 多重继承
- 抽象类只能单继承(一个类只能
extends一个抽象类) - 接口支持多重实现(一个类可以
implements多个接口)
5.5. 类与类的关系
5.5.1. 继承/泛化
子类继承父类的属性和方法,表示一种泛化的关系
class Animal {
speak() {
console.log("Animal makes a sound");
}
}
class Dog extends Animal {
speak() {
console.log("Dog barks");
}
}
const dog = new Dog();
dog.speak(); // Dog barks
5.5.2. 组合
一个类包含另一个类的实例,表示整体-部分的关系
class Engine {
start() {
console.log("Engine started");
}
}
class Car {
constructor() {
this.engine = new Engine(); // Car 组合 Engine
}
start() {
this.engine.start();
console.log("Car started");
}
}
const myCar = new Car();
myCar.start();
Car拥有Engine,但是 Engine 不是 Car 的子类,而是 Car 的一个组成部分。
5.5.3. 关联
一个类使用另一个类的对象,但它们互相是独立的,没有所有权关系
class Driver {
drive(car) {
console.log("Driving the car...");
car.start();
}
}
const driver = new Driver();
driver.drive(myCar);
Driver使用Car,但 Driver 并不拥有Car。
5.5.4. 聚合
弱拥有关系:一个类包含另一个类的实例,但生命周期是独立的
class Team {
constructor() {
this.members = [];
}
addMember(member) {
this.members.push(member);
}
}
class Player {
constructor(name) {
this.name = name;
}
}
const team = new Team();
const player1 = new Player("Alice");
team.addMember(player1);
Team 拥有 Player,但 Player 的生命周期不依赖于 Team。
5.5.5. 依赖
一个类的方法参数或者局部变量中使用了另一个类
class PaymentProcessor {
process(paymentMethod) {
paymentMethod.pay();
}
}
class CreditCard {
pay() {
console.log("Paid with credit card");
}
}
const processor = new PaymentProcessor();
processor.process(new CreditCard());
PaymentProcessor 依赖于 CreditCard,但并没有直接拥有 CreditCard 的实例。
5.6. 数据与JSON的转换
引入包:dart:convert
jsonEncode:map转JSON
jsonDecode:JOSN转map,返回对象类型为Map<String,dynamic>
5.6.1. 什么是JSON
JSON就是特定格式的字符串。我们可以将数据按照这个格式进行封装,然后可以在不同的语言和系统之间进行传送和交互。
{
"sid": "001",
"name": "zhangsan",
"age": 18
}
[ { key1:value1, key2:value2 }, { key3:value3, key4:value4 }]
5.6.2. Map(字典) ↔️ JSON
jsonEncode:map转JSON
jsonDecode:JOSN转map,返回对象类型为Map<String,dynamic>
import 'dart:convert';
void main() {
Map<String, dynamic> data = {
'name': '逗比',
'age': 25,
};
String jsonString = jsonEncode(data);
print(jsonString); //{"name":"逗比","age":25}
String jsonString1 = '{"name": "逗比", "age": 25}';
Map<String, dynamic> data = jsonDecode(jsonString1);
print(data['name']); //逗比
print(data['age']); //25
}
5.6.3. 对象 ↔️ JSON
对象转JSON:需要先把对象转成Map,然后再调用jsonEncode,要不然jsonEncode会报错(Instance of 'Person')
import 'dart:convert';
class Person {
String name;
int age;
Person(this.name, this.age);
Map<String, dynamic> toJson() {
return {'name': name, 'age': age};
}
}
void main() {
Person person = Person("小红", 19);
var data = jsonEncode(person.toJson());
print(data);
}
JSON转对象:先把JSON转成Map,然后在调用对象构造函数转成对象
import 'dart:convert';
class Person {
String name;
int age;
Person(this.name, this.age);
factory Person.mapToObject(Map<String, dynamic> map) {
return Person(map["name"], map["age"]);
}
}
void main() {
String jsonStr = '{"name":"allen","age":20}';
Map<String, dynamic> map = jsonDecode(jsonStr);
Person person = Person.mapToObject(map);
print(person); //Instance of 'Person'
print(person.name); //allen
print(person.age); //20
}
5.6.4. List ↔️ JSON
List转JSON:直接调用jsonEncode就可以
import 'dart:convert';
void main() {
List<dynamic> list = [
"hahah",
123,
true,
{"name": "allen", "age": 30},
];
print(jsonEncode(list)); //["hahah",123,true,{"name":"allen","age":30}]
}
JSON转List:
① 不含map的json字符串:直接调用jsonEncode转就行
import 'dart:convert';
void main() {
String json = '["hahah",123,true]';
print(jsonDecode(json)); //[hahah, 123, true]
}
② 含有对象列表的json字符串:先将json转成List,然后再循环list调用对象的构造函数转成指定的对象.toList(),然后就转换成功啦。
import 'dart:convert';
class Person {
String name;
int age;
Person(this.name, this.age);
factory Person.fromJson(Map<String, dynamic> json) {
return Person(json['name'], json['age']);
}
}
void main() {
String jsonString = '[{"name":"逗比","age":12},{"name":"二比","age":13},{"name":"牛比","age":14}]';
// 首先将JSON转换为List<dynamic>
List<dynamic> jsonDynamicList = jsonDecode(jsonString);
// 然后遍历List<dynamic> 将其中的元素分别转换为Person对象
List<Person> personList =
jsonDynamicList.map((json) => Person.fromJson(json)).toList();
for (var person in personList) {
print('Name: ${person.name}, Age: ${person.age}');
}
}
6. 泛型
6.1. 泛型定义
Dart中的泛型,可以将类型参数化,从而实现更加通用和可复用的代码。
泛型允许我们编写一次代码,使用多种类型,提高了代码的灵活性和可维护性。
通俗理解:
解决类、接口、方法的复用性,以及对不特定数据类型的支持(类型校验)
6.2. 泛型类
平常使用的方式:
class Student {
String name;
int age;
Student(this.name, this.age);
}
class Teacher {
String name;
int age;
Teacher(this.name, this.age);
}
class DataResult {
int messageCode;
Object? data;
// 默认构造方法
DataResult(this.messageCode, this.data);
// 错误的时候,返回错误码,没有数据
DataResult.error(this.messageCode);
}
import 'DataResult.dart';
import 'Student.dart';
import 'Teacher.dart';
void main() {
// 封装数据
Student stu = Student("Doubi", 12);
DataResult result1 = DataResult(200, stu);
Teacher tea = Teacher("Niubi", 32);
DataResult result2 = DataResult(200, tea);
// 获取数据
if (result1.data.runtimeType == Student) {
Student stu2 = result1.data as Student; // 需要进行类型转换
print(stu2.name);
}
if (result2.data.runtimeType == Teacher) {
Teacher tea2 = result2.data as Teacher; // 需要进行类型转换
print(tea2.name);
}
}
上面的代码:先将学生和老师的信息封装到DataResult中。然后重新从DataResult获取老师和学生的信息
但是:获取到的信息需要使用as进行类型转换,如果使用Student stu2 = result1.data;直接赋值代码会报错,因为data是Object类型。为了安全起见,我们使用runtimeType进行类数据的类型判断,然后再进行转换。
修改代码
class DataResult<T> {
int messageCode;
T? data;
DataResult(this.messageCode, this.data);
DataResult.error(this.messageCode);
}
import 'DataResult.dart';
import 'Student.dart';
import 'Teacher.dart';
void main() {
// 封装数据
Student stu = Student("Doubi", 12);
DataResult<Student> result1 = DataResult(200, stu);
Teacher tea = Teacher("Niubi", 32);
DataResult<Teacher> result2 = DataResult(200, tea);
Student stu2 = result1.data!; // 不需要进行类型转换
print(stu2.name);
Teacher tea2 = result2.data!; // 不需要进行类型转换
print(tea2.name);
}
这个在创建DataResult对象的时候,使用泛型,然后再通过对象来获取data的时候,就不用类型转换了
6.3. 泛型函数
是一种具有泛型类型参数的函数,可以在函数签名中使用类型参数来实现更通用和灵活的代码。泛型函数可以在参数、返回类型或者函数体中使用这些类型参数。
// 获取列表的最后一个元素
T getLast<T>(List<T> list) {
return list[list.length - 1];
}
void main() {
List<int> intList = [1, 2, 3];
List<String> strList = ['a', 'b', 'c', 'd'];
int i = getLast<int>(intList);
print(i);
String s = getLast<String>(strList);
print(s);
}
6.4. 泛型接口
可以具有泛型类型参数的接口,允许在接口定义中使用属性参数,并在实现接口时指定参数的类型
/ 定义一个泛型接口
abstract class Repository<T> {
void save(T data);
T findById(int id);
}
// 实现泛型接口
class UserRepository implements Repository<String> {
@override
void save(String data) {
print('Saving user: $data');
}
@override
String findById(int id) {
return 'User with ID $id';
}
}
void main() {
UserRepository userRepository = UserRepository();
userRepository.save('John Doe');
String user = userRepository.findById(1);
print(user);
}
6.5. 泛型约束
使用泛型的时候,还可以使用extends关键字来进行约束
// 获取列表的最后一个元素
T getLast<T extends num>(List<T> list) {
return list[list.length - 1];
}
void main() {
List<int> intList = [1, 2, 3];
int i = getLast(intList);
print(i);
List<double> strList = [1.0, 2.0, 3.0];
double d = getLast(strList);
print(d);
//String s = getLast<String>(strList); // 代码报错,String不是num的子类
}
上面定义的泛型<T extends num>表示泛型T必须是num的子类型
所以在调用的时候,列表的数据类型需要是num的子类,例如int和Double,如果设置为其他类型就会报错(例如:传递String)
7. 函数式编程
7.1. 函数作为参数
匿名函数也可以作为函数的参数
7.1.1. 函数赋值给变量
int compute(x, y) {
// 定义一个函数
return x + y;
}
void main() {
Function func = compute; // 将函数赋值给变量,注意函数名称后面没有括号
int result = func(1, 2); // 通过变量调用函数
print(result);
}
函数后面没有括号,有括号就调用函数了
函数赋值给变量后,可以通过变量调用函数
7.1.2. 函数作为参数
int plus(x, y) {
// 定义一个加法的函数
return x + y;
}
int multiply(x, y) {
// 定义一个乘法的函数
return x * y;
}
int calculate(x, y, func) {
// 第三个参数是一个函数,在该函数中调用了这个函数
return func(x, y);
}
void main(List<String> args) {
int result1 = calculate(1, 2, plus); // 传递三个参数,第三个参数是一个函数,使用第三个参数的函数来计算前两个参数
print(result1);
int result2 = calculate(1, 2, multiply);
print(result2);
}
这样根据传入的函数的不同,实现了不同的逻辑,这是计算逻辑的传递,而不是数据的传递。
7.1.2.1. 应用
import 'Student.dart';
// 根据条件获取学生列表
List<Student> getStuListByCondition(
List<Student> allStuList, Function funCondition) {
List<Student> stu_list = [];
for (Student stu in allStuList) {
if (funCondition(stu)) {
stu_list.add(stu);
}
}
return stu_list;
}
// 获取语文成绩不及格的同学
bool getChineseFlunkCondition(stu) {
return stu.chinese < 60;
}
// 获取所有课程不及格的同学
bool getFlunkCondition(stu) {
return stu.chinese < 60 && stu.math < 60 && stu.english < 60;
}
// 获取所有课程都是优秀的学生
bool getExcellentCondition(stu) {
return stu.chinese >= 90 && stu.math >= 90 && stu.english >= 90;
}
// -------------方法调用---------------------
void main() {
// 学生列表
List<Student> allStuList = [ Student("zhangsan", 87, 48, 92), Student("lisi", 47, 92, 71), Student("wangwu", 58, 46, 38), Student("zhangliu", 95, 91, 99) ];
print("语文不及格的同学:");
List<Student> stuList =
getStuListByCondition(allStuList, getChineseFlunkCondition);
for (Student stu in stuList) {
print(stu);
}
print("所有成绩都不及格的同学:");
stuList = getStuListByCondition(allStuList, getFlunkCondition);
for (Student stu in stuList) {
print(stu);
}
print("所有成绩都是优秀的同学:");
stuList = getStuListByCondition(allStuList, getExcellentCondition);
for (Student stu in stuList) {
print(stu);
}
}
7.1.3. lambda表达式
(参数列表) => 表达式
方法体不需要return语句,会自动返回。
lambda就是为了将方法作为参数而存在的,当然如果你将lambda表达式赋值给一个变量,也不是不可以,只是有点没事找抽,那还不如定义成原来函数的模样。
ambda表达式的方法体只能有一行代码,无法写多行。(箭头函数可以写多行)
7.2. 函数作为返回值
7.2.1. 闭包
闭包必须满足三个条件:
- 在一个外部函数中有一个内部函数
- 内部函数必须引用外部函数中的变量
- 外部函数的返回值必须是内部函数
Function outer_func() {
int a = 1;
void inner_func() {
print(a); // 使用了外部函数的变量
}
return inner_func; // 将内部函数返回,这里函数名后没有括号
}
void main() {
var result = outer_func(); // 这里得到的结果是一个函数
result(); // 执行内部函数,结果为:1
}
7.2.2. 函数之间共享数据
使用闭包可以在内部函数和外部函数之间共享数据。
Function counter() {
int count = 0;
int inner() {
count += 1;
return count;
}
return inner;
}
void main() {
var c1 = counter(); // 创建了一个计数器
print(c1()); // 输出 1
print(c1()); // 输出 2
var c2 = counter(); // 创建了第二个计数器
print(c2()); // 输出 1
}
7.2.3. 作为回调函数
回调函数指的是将一个函数作为参数传递给另外一个函数,然后在这个函数中执行这个参数函数。回调函数通常用于异步编程中,可以在事件发生后回调执行,以完成一些特定的任务。例如:在实际的功能实现中,我们经常会做一些耗时的操作,例如网络请求,发起网络请求后,继续执行后面的代码,待服务器返回结果后,在通过回调函数的方式返回结果。
void plus(a, b, callback) {
// callback参数就是一个函数
int result = a + b;
callback(result); // 执行callback函数将结果返回
}
void main() {
plus(10, 20, (int result) {
print("计算结果:$result");
}); // 计算结果:30
}
8. 库
在Dart中,库的使用时通过import关键字引入的。
library指令可以创建一个库,每个Dart文件都是一个库,即使没有使用library指令来指定。
Dart中的库主要有三种:
- 自定义的库
- 系统内置的库:
import '目录名/文件名.dart' - Pub包管理系统中的库
8.1. 自定义的库
import '目录名/文件名.dart'
8.2. 系统内置的库
其实dart默认是导入了 dart:core 库的, dart:core 是 Dart 的核心库,包含了 Dart 的基本数据类型、集合类型、函数和异常处理等基本功能,在编码的时候无需显式导入即可使用。
import 'dart:库名';
//常用
import 'dart:math';
import 'dart:io';
import 'dart:convert';
import 'dart:io';
import 'dart:convert';
void main() async{
var result = await getDataFromZhihuAPI();
print(result);
}
//api接口: http://news-at.zhihu.com/api/3/stories/latest
getDataFromZhihuAPI() async{
//1、创建HttpClient对象
var httpClient = new HttpClient();
//2、创建Uri对象
var uri = new Uri.http('news-at.zhihu.com','/api/3/stories/latest');
//3、发起请求,等待请求
var request = await httpClient.getUrl(uri);
//4、关闭请求,等待响应
var response = await request.close();
//5、解码响应的内容
return await response.transform(utf8.decoder).join();
}
8.3. pub包管理系统的库
pub 包管理系统是官方的包管理工具,用于查找、安装和发布 Dart 包。
https://pub.dev/packages
https://pub.flutter-io.cn/packages
https://pub.dartlang.org/flutter/
步骤:
- 首先可以在上面的网页中查找自己需要的库
- 需要在自己的项目根目录创建一个
pubspec.yaml文件;在pubspec.yaml文件中,配置依赖的库的信息,包括名称、版本、描述等; - 在根目录下运行
dart pub get命令,将库下载到本地 - 然后就可以在代码中引入库中的功能了。
8.3.1. 查找库
根据上面的网址查看自己的库
会有安装的方式,但是我们用yaml去安装就可以了
8.3.2. 新建 pubspec.yaml 文件
name: hello_dart # 项目名称
description: My Dart application # 项目描述
version: 1.0.0 # 项目版本
environment: # 指定sdk版本约束
sdk: '>=2.12.0 <3.0.0'
dependencies:
http: ^0.13.6
dev_dependencies:
# 添加开发时需要的依赖包,如果有的话
有的依赖对SDK版本有要求,例如当dart版本是3.0以下是,引入http1.0.0就会有如下错误:
ecause hello_dart depends on http >=1.0.0 which requires SDK version >=3.0.0 <4.0.0, version solving failed.
8.3.3. 下载依赖
在上面保存 pubspec.yaml 文件的时候,VSCode就会自动下载依赖。
我们也可以在根目录下运行 dart pub get 命令来下载依赖;
8.3.4. 引入和使用依赖
下载依赖后,就可以引入依赖并使用了,举个栗子:
import 'dart:convert' as convert;
import 'package:http/http.dart';
void main(List<String> arguments) async {
var url = Uri.http('www.doubibiji.com', '/apis/category/allNoteList', {});
var response = await get(url);
if (response.statusCode == 200) {
var jsonResponse =
convert.jsonDecode(response.body) as Map<String, dynamic>;
print('http result: $jsonResponse');
} else {
print('Request failed with status: ${response.statusCode}.');
}
}
在上面的代码中使用 import 'package:http/http.dart'; 引入了http。
其中的函数 get(url) 就是http提供的,用于发起 get 请求。
关于 await 的使用,后面异步有讲解,这里知道它在等待请求的信息即可。
8.4. 库名冲突
引入的多个库存在相同名称的内容的时候,我们可以使用 as 关键字来避免冲突。
import 'domain/Person.dart' as Per1;
import 'pojo/Person.dart' as Per2;
void main(List<String> args) {
Per1.Person p1 = Per1.Person("Doubi", 12);
Per2.Person p2 = Per2.Person("Niubi", 13);
}
在上面我们使用http请求的时候,一般也是会起个别名,防止冲突:
// 引入的时候使用别名,防止冲突
import 'package:http/http.dart' as http;
// 然后使用http.get()
var response = await http.get(url);
8.5. 部分导入
如果希望只导入库中某些内容,我们可以使用 show 和 hide 关键字。
show关键字:只导入需要的部分
hide关键字:隐藏不需要的部分
🌰
import 'package:http/http.dart' as http;
void main(List<String> arguments) async {
var url = Uri.http('www.doubibiji.com', '/apis/category/allNoteList', {});
var response1 = await http.get(url);
var response2 = await http.post(url);
}
import 'package:http/http.dart' as http show get;
这样 http.post(url); 将会报错。
import 'package:http/http.dart' as http hide get;
这样将不会导入 get 函数,http.get(url); 将会报错。
8.6. 延迟加载
延迟加载也称为懒加载,可以在需要的时候再进行加载。懒加载的最大好处是可以减少APP的启动时间。懒加载使用deferred as关键字来指定
import 'package:deferred/hello.dart' deferred as hello;
当需要使用的时候,需要使用loadLibrary()方法来加载
greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
8.7. 库的定义
如果一个库文件太大,将所有内容保存到一个文件中是不太合理的,我们有可能希望将这个库进行拆分,每个文件都包含库中的一部分代码,这样可以更好地组织和管理大型项目中的代码。
之前我们可以使用part关键字,但是现在不推荐了,替代方案是使用export。
🌰
// utils.dart
int add(int a, int b) {
return a + b;
}
class Point {
int x;
int y;
Point(this.x, this.y);
}
拆分
// math_utils.dart
int add(int a, int b) {
return a + b;
}
// geometry_utils.dart
class Point {
int x;
int y;
Point(this.x, this.y);
}
现在已经将 utils.dart 库中的内容分到两个文件中了,现在需要使用 export 关键字将这两个文件中的公共部分暴露给其他库,编辑 utils.dart 为:
// utils.dart
library my_utils;
export 'math_utils.dart';
export 'geometry_utils.dart';
我们现在在 utils.dart 中,将 math_utils.dart 和 geometry_utils.dart 导出,然后使用 library 关键字定义库的名称。
library 关键字的作用主要是为了将多个相关的 .dart 文件组织为一个库。在大多数情况下,特别是在简单的项目中或者所有相关文件都在同一个文件夹下时,library 关键字是可选的,不写也不会产生影响。所以上面 library my_utils; 这句代码,不写也没有影响。
9. 异常
在程序执行过程中出现的错误或异常的情况,导致程序无法正常执行的事件。
Dart提供了异常处理机制来捕获和处理这些异常
当程序出现错误的时候,我们通常称之为:抛出异常。
程序抛出异常就无法继续执行了,但是任何程序都不可能是完美的没有bug的,只能尽可能的对错误进行预防和处理。
9.1. 捕获异常
对异常进行预防和提前处理,这种行为通常称为异常捕获
一个程序出现任何错误,就停止运行肯定不是我们希望看到的,即使程序出现错误,哪怕给用户一个错误提示,也是一个积极处理问题的方法
9.1.1. 异常捕获语法
在出现异常语句后的代码是无法被继续执行的。
try{
}catch(Exception e){
}
9.1.2. 打印异常详细信息
捕获异常后,在catch块中,可以通过异常对象和stacktrace对象打印异常的详细信息。
mport 'dart:convert';
void main() {
try {
String jsonString = 'Hello';
// 将字符串转换为字典
Map<String, dynamic> data = jsonDecode(jsonString);
} catch (e, stackTrace) {
print("JSON字符串格式不正确");
print('发生异常: $e');
print('堆栈轨迹:\n$stackTrace');
}
}
9.1.3. 捕获指定类型的异常
在程序运行时,可以需要对不同类型的异常进行不同的处理,这个时候就需要根据类型来捕获异常了。
try {
String jsonString = 'Hello';
//将字符串转换为字典
Map<String,dynamic> data = jsonDecode(jsonString);
} on FormatException catch(e){
//捕获并处理异常
print("JSON字符串格式不正确");
print("异常信息:$e");
}
上面捕获了FormatException类型的异常,在捕获异常后,打印了一场信息和提示信息
如果不想获取异常信息,还可以省略catch(e):
try {
String jsonString = 'Hello';
// 将字符串转换为字典
Map<String, dynamic> data = jsonDecode(jsonString);
} on FormatException {
// 捕获并处理异常
print("JSON字符串格式不正确");
}
需要注意,上面只是捕获了 FormatException 类型的异常,如果有代码抛出了其他类型的异常,是无法捕获的。
🌰
try {
// 截取字符串
String subStr = "Hello".substring(10);
print("子字符串:$subStr");
String jsonString = 'Hello';
// 将字符串转换为字典
Map<String, dynamic> data = jsonDecode(jsonString);
} on FormatException {
// 捕获并处理异常
print("JSON字符串格式不正确");
}
就像这个,截取字符串超出长度,就会抛出异常,但是我们没有捕获这个异常,导致程序终止
所以针对这种情况需要捕获多种异常。
9.1.4. 捕获多个异常
可以同时捕获多个异常,并对每种异常可以采用不同的处理。
import 'dart:convert';
void main() {
try {
// 截取字符串
String subStr = "Hello".substring(10);
print("子字符串:$subStr");
String jsonString = 'Hello';
// 将字符串转换为字典
Map<String, dynamic> data = jsonDecode(jsonString);
} on FormatException catch (e) {
// 捕获并处理异常
print("JSON字符串格式不正确");
print("错误信息:$e");
} on RangeError catch (e) {
print("截取字符串越界");
print("错误信息:$e");
} catch (e) {
print("错误信息:$e");
}
}
如果我们不能判断是否还会抛出其他类型的错误,可以在最后添加 catch (e) 用来捕获其他类型的错误。
9.1.5. 异常finally
finally表示无论是否发生异常都要执行的代码
try {
String jsonString = 'Hello';
// 将字符串转换为字典
Map<String, dynamic> data = jsonDecode(jsonString);
} catch (e) {
// 捕获并处理异常
print("JSON字符串格式不正确");
} finally {
print("出现异常也会被执行");
}
finally 一般在文件读写操作的时候用的比较多,就是不管是否发生错误都要关闭文件,所以可以将文件的关闭操作放在finally中
9.2. 异常的传递
异常的传递,就是当函数或方法执行的时候出现异常,如果没有进行捕获,就会将异常传递给该函数或方法的调用者,如果调用者仍未处理异常,则继续向上传递,直到传递给主程序,如果主程序仍然没有进行异常处理,则程序将被终止。
import 'dart:convert';
void parseJson() {
String jsonString = 'Hello';
// 将字符串转换为字典
Map<String, dynamic> data = jsonDecode(jsonString);
}
void callFun() {
// 调用上面的input_fun()
parseJson();
}
void main() {
try {
parseJson();
print("无法被指定到的代码");
} catch (e) {
// 捕获并处理异常
print("JSON字符串格式不正确");
}
}
上面代码,执行parseJson会抛出异常,因为parseJson()函数中没有进行异常处理,则异常会抛给callFun()函数,callFun()没有进行异常处理,则抛给main()函数,main()函数中则对异常进行了处理,程序不会崩溃。
利用异常的传递性,如果我们在main()函数中进行了异常捕获,无论程序哪里发生了错误,最终都会被传递到main()函数中,保证所有的异常都会被捕获。但是不要所有的异常都在main函数中处理,main函数中的异常处理只是兜底处理。
9.3. 主动抛出异常
除了代码执行的时候出错,系统会主动抛出异常,我们还可以根据实际的业务需要,主动抛出异常
首先创建一个异常对象,然后使用throw关键字抛出异常对象
import 'dart:io';
String inputUsername() {
print("请输入用户名:");
// 读取键盘输入
var name = stdin.readLineSync();
if (null == name || name.length > 16 || name.length < 8) {
throw Exception("用户名格式错误"); // 主动抛出异常
}
return name;
}
void main() {
try {
var password = inputUsername();
print("输入的用户名:$password");
} catch (e) {
print("用户名格式错误,请重新输入");
}
}
当输入abc的时候,会主动抛出错误
9.4. 自定义异常类
除了使用 Dart 内置的异常类型外,您还可以自定义异常类,以便更好地描述特定的异常情况。自定义异常类通常继承自Exception或其子类。建议以Exception结尾
import 'dart:io';
class UsernameEmptyException implements Exception {
// 自定义异常类
String errMsg() => '用户名为空';
}
class UsernameLengthException implements Exception {
int minLength;
int maxLength;
String message;
// 自定义异常类
UsernameLengthException(this.message, this.minLength, this.maxLength);
String errMsg() =>
'${this.message}, minLength:${this.minLength}, maxLength:${this.maxLength}';
}
String inputUsername() {
print("请输入用户名:");
// 读取键盘输入
var name = stdin.readLineSync();
if (null == name || name.length < 1) {
throw UsernameEmptyException(); // 抛出异常
}
if (name.length > 16 || name.length < 8) {
throw UsernameLengthException("用户名格式错误", 8, 16); // 抛出异常
}
return name;
}
void main() {
try {
var password = inputUsername();
print("输入的用户名:$password");
} on UsernameEmptyException {
print("用户名不能为空");
} on UsernameLengthException catch (e) {
print("${e.errMsg()}");
} catch (e) {
print(e);
}
}
10. 异步
10.1. 异步和同步
同步:代码按照顺序一步一步执行,每一步都会等待前一步完成后再进行,如果遇到耗时的操作,程序会阻塞等待,直到前一步执行完成,才会继续执行后续代码的方式,这种方式叫做 同步。同步 意味着如果有耗时操作,程序就会阻塞。
异步:如果 函数执行的时候,不阻塞等待函数执行完成,就继续执行后面的代码,也就是存在两个任务同时执行,这种方式叫做 异步,异步 是非阻塞的。
问题:在实际开发中如何处理耗时的操作呢?
在Java或C++等程序中,是通过开启子线程,将耗时的操作放在子线程中进行处理,子线程和主线程同时运行,实现异步操作,子线程处理完成,在将结果交给主线程。
但是Dart默认是单线程的,该如何进行异步操作呢?
10.2. Dart中的异步
Dart中是使用 单线程 + 事件循环 来实现异步操作的。
在单线程模型维护着一个事件循环(Event Loop),当有网络请求、文件读写IO操作等事件时,会将这些事件加入到事件队列中,事件循环会不断的从事件队列中取出事件并处理,直到事件队列清空。
主要关键字
Future async await
同步代码
import "dart:io";
import 'dart:convert';
main() {
print("main start");
print(requestNetwork());
print("main end");
}
String requestNetwork() {
sleep(Duration(seconds: 3)); // 休眠3秒,模拟从网络获取数据
Map<String, dynamic> data = {
'name': '逗比',
'age': 25,
};
String jsonString = jsonEncode(data);
return jsonString;
}
运行结果:
main start
{"name":"逗比","age":25}
main end
10.2.1. Future
import "dart:io";
import 'dart:convert';
main() {
print("main start");
print(requestNetwork());
print("main end");
}
Future<String> requestNetwork() {
return new Future(() {
sleep(Duration(seconds: 3)); // 休眠3秒,模拟从网络获取数据
Map<String, dynamic> data = {
'name': '逗比',
'age': 25,
};
String jsonString = jsonEncode(data);
return jsonString;
});
}
执行结果:
main start
Instance of 'Future'
main end
再执行的时候,会发现很快执行完成,没有看到阻塞的现象,这是因为使用了Future对象将耗时的操作隔离了起来,不会影响主线程。
但是打印的请求网络的结果是一个Future对象,并不是最终的结果,那么如果获取最终结果呢?
- 创建一个
Future对象,将异步操作通过函数传给Future对象; - 通过
Future对象的.then方法可以获取到事件执行完成的结果; - 通过
.catchError方法可以监听 Future 内部执行失败或者出现异常时的错误信息;
🎈通过 .then回调获取异步结果
var future = requestNetwork();
future.then((value) {
print("请求结果:$value");
});
执行结果:
main start
main end
请求结果:{"name":"逗比","age":25}
🎈通过 .catchError来捕获异常
var future = requestNetwork();
future.then((value) {
print("请求结果:$value");
}).catchError((error) {
print("捕获异常:$error");
});
🎈链式调用:可以解决回调地狱的问题
回调地狱 : 回调地狱(Callback Hell)是指在异步编程中,多个异步操作嵌套在一起,形成多层的回调函数,使得代码看起来非常复杂、难以维护和理解的情况。在回调地狱中,每个异步操作都需要在前一个异步操作完成后进行,并且依赖于前一个异步操作的结果。
add(6, 4).then((value) {
print("加法的结果:$value");
subtract(value, 4).then((value) {
print("减法的结果:$value");
multiply(value, 2).then((value) {
print("乘法的结果:$value");
divide(value, 4).then((value) {
print("除法的结果:$value");
});
});
});
});
add(6, 4).then((value) {
print("加法的结果:$value");
return subtract(value, 4);
}).then((value) {
print("减法的结果:$value");
return multiply(value, 2);
}).then((value) {
print("乘法的结果:$value");
return divide(value, 4);
}).then((value) {
print("除法的结果:$value");
}).catchError((error) {
print("Error: $error"); // 捕获异常
});
🎈其它方法
Future.value()
Future.value(value)是用于创建一个立即完成,并且结果为指定值的Future对象的方法。这个方法可以在不涉及异步操作的情况下创建一个Future,使其立即完成,并将指定的值作为结果返回。
main() {
print("main start");
var future = Future.value(42);
future.then((value) {
print(value);
});
print("main end");
}
执行结果:
main start
main end
42
使用场景
- 统一接口: 在某些情况下,可能希望将异步和同步操作都通过
Future的方式进行处理,以保持代码的一致性。当您有一个函数需要返回Future类型的结果,而实际上该结果是同步操作的结果时,那么就可以使用Future.value(value)将同步操作的结果包装为立即完成的Future,以便统一返回类型的要求。 - 模拟: 在单元测试中,有时候可能需要模拟一个返回
Future的函数,以便测试异步操作的情况。我们可以使用Future.value(value)来创建一个模拟的Future对象,以返回预定的结果。 - 函数式编程: 在函数式编程风格中,有时候可能会使用
Future作为一种容器,用于将异步和同步操作一致地处理。在这种情况下,您可以使用Future.value(value)来包装同步操作的结果,以便将其纳入函数式编程的范畴。
10.2.2. async、await
什么是async、await?
async、await可以让我们用同步的代码格式,去实现异步的调用过程。
import 'dart:convert';
void main() async {
print("main start");
fetchPersonInfo();
print("main end");
}
void fetchPersonInfo() {
var info = requestNetwork();
print("info:$info");
}
Future<String> requestNetwork() async {
var result = await Future.delayed(Duration(seconds: 3), () {
Map<String, dynamic> data = {
'name': '逗比',
'age': 25,
};
String jsonString = jsonEncode(data);
return jsonString;
});
print("result:$result");
return result;
}
执行结果:
main start
info:Instance of 'Future<String>'
main end
result:{"name":"逗比","age":25}
执行的结果发现,在fetchPersonInfo()函数中调用requestNetwork()并没有拿到最终的结果,info变量还是一个Future对象
这是因为代码执行到await会立即返回一个future对象,也就是赋给info对象,而在 requestNetwork() 函数中继续等待 Future 的执行,执行完成再返回最终的结果。
question:fetchPersonInfo()怎么拿到最终的结果???
此时 fetchPersonInfo() 调用 requestNetwork() 还是异步的,我们只需要在 fetchPersonInfo() 中等待 requestNetwork() 执行结果就行,在调用 requestNetwork() 的前面也加上 await 。
🔴总结
- 在异步操作的前面可以添加
await关键字,会等待异步操作执行的结果; await只能在async方法中使用,所以方法要添加async关键字;- 代码执行到
await会立即返回一个future对象给调用者,然后等待异步执行,执行完成,才会执行await后面的代码,最终得到异步的结果返回。 - 代码用了
同步的代码格式,去实现异步的调用过程。
🌰接口请求实例
import 'package:http/http.dart' as http;
main() {
print("main start");
getWebsiteStatus();
print("main end");
}
Future getWebsiteStatus() async {
print("开始获取网站的状态");
int status = await getHttp();
if (status == 200) { // 200表示可用
print("网站可用");
} else {
print("网站不可用");
}
}
Future<int> getHttp() async {
try {
final result = await http
.get(Uri.http('www.doubibiji.com', '/', {}))
.timeout(Duration(seconds: 5)); // 设置请求超时时间为5秒
// 获取的状态码
return result.statusCode;
} catch (e) {
print("请求出错: $e");
return -1;
}
}
执行结果:
main start
开始获取网站的状态
main end
网站可用
10.2.3. 微任务队列
事件循环(Event loop)里面存在一个事件队列(Event Queue),事件循环不断从循环不断从事件队列中取出事件执行。
严格划分的话,在Dart中还存在一个队列:微任务队列。微任务队列的优先级要高于事件队列,也就是说事件循环会优先执行微任务队列中的任务,再执行事件队列中的任务
import "dart:async";
main() {
print("main start");
scheduleMicrotask(() {
print("你好,逗比");
});
print("main end");
}
scheduleMicrotask() 参数的匿名函数会被加入到微任务队列中等待执行。
执行结果:
main start
main end
你好,逗比
微任务队列和事件队列做一个对比:
import "dart:async";
main() {
print("main start");
Future(() {
print("会被加入到事件队列中");
}).then((value) {
print("执行then");
});
scheduleMicrotask(() {
print("你好,逗比");
});
print("main end");
}
Future 在微任务队列之后才被执行,这是因为 Future 构造函数传入的函数体放在事件队列中被执行的。
而then 注册的回调函数会被添加到微任务队列中,以确保它们在当前任务完成后尽快执行,而不会受到事件队列中的其他任务的延迟影响。虽然 then 方法的执行不会直接被添加到事件队列,但在某些情况下,可能会触发与事件队列相关的操作。例如,如果在 then 注册的回调函数中执行了某些异步操作,那么这些异步操作可能会在事件队列中注册并执行。但是,then 本身注册的回调函数仍然会被添加到微任务队列中。
10.3. 多线程
在Dart中,默认是单线程的,每个线程实际上试运行在一个Isolate中的。每个Isolate都有自己独立的执行线程和事件循环,以及私有的内存空空间。
而一个Isolate只能利用一个CPU内核。所以Dart默认单线程的情况下,只能利用一个CPU内核,这对于多核CPU来说,是一种资源的浪费。
如果希望在多个CPU内核上并行运行多个任务,我们那可以创建多个Isolate。每个Isolate都是独立的执行单元,可以在一个单独的CPU内核上运行。通过创建多个Isolate,Dart可以更好的利用多核处理器,提高应用程序的性能。
如果某个操作计算量很大,以至于他在主线程的Isolate运行中会导致阻塞或者掉帧,我们可以创建独立的isolate来做密集计算,让主Isolate专注重建和渲染。
10.3.1. 创建Isolate
通过Isolate.spawn创建Isolate
import "dart:isolate";
main() {
Isolate.spawn(foo, "hello,word");
}
void foo(info) {
print("isolate:$info");
}
上面的代码中使用了 Isolate.spawn 创建了一个新的 Isolate ,并指定了一个函数作为参数,并给函数传递了参数。程序会在新的 Isolate 中执行。
执行的结果:
isolate:hello,word
注意:各个Isolate之间是独立的执行单元,它们之间的数据是相互隔离的,而且不会直接共享内存。
import 'dart:isolate';
int a = 0;
void main() async {
print('main start');
// 创建一个 Isolate
Isolate.spawn(computeAdd, 10);
print(a);
print('main end');
}
void computeAdd(int n) {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
a = result;
print('1到$n的和: $result');
}
因为 int a = 0; 是在主 Isolate 中定义的一个变量,然后在另一个 Isolate 中的 computeAdd 函数中修改了 a 的值。但由于 Isolate 之间没有直接共享内存,这种方式是不会影响主 Isolate 中的 a 值的。因此,在打印 a 时看到的结果仍然是初始值 0,而不是计算后的结果。
执行结果:
main start
1到10的和: 55
0
main end
10.3.2. Isolate通讯
一个🌰
import 'dart:isolate';
void main() async {
print('main start');
// 创建一个 Isolate
Isolate.spawn(computeAdd, 9999999);
print('main end');
}
void computeAdd(int n) {
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
print('1到$n的和: $result');
}
执行结果:
main start
main end
没有输出结果,原因:上面计算时间太长,主线程执行完了,导致整个Dart程序结束,从而终止所有的Isolate
所以这里要用到Isolate的通讯机制,使用ReceivePort来让主线程等待从其他Isolate发送的消息,以保持主Isolate的运行状态。
在主线程中,将main isolate的发送管道(SendPort)传递给新创建的Isolate,当Isolate执行完毕时,可以利用这个管道给Main isolate发送消息。
import 'dart:isolate';
void main() async {
print('main start');
final receivePort = ReceivePort();
// 传递多个参数的时候可以使用字典
Isolate.spawn(
computeAdd, {'n': 1000000000, 'sendPort': receivePort.sendPort});
receivePort.listen((result) {
print('计算结果: $result');
// 不再使用时,我们会关闭管道
receivePort.close();
});
print('main end');
}
// 传递多个参数的时候可以使用字典
void computeAdd(Map<String, dynamic> args) {
final n = args['n'];
final sendPort = args['sendPort'];
int result = 0;
for (int i = 1; i <= n; i++) {
result += i;
}
sendPort.send(result);
}
首先创建了一个 ReceivePort, 然后将 sendPort 传递给了新创建的 Isolate,如果想传递多个参数给新创建的 Isolate 可以使用字典。然后使用 ReceivePort 开启监听,监听来自新创建 Isolate 的消息。在新创建的 Isolate 中,可以使用 sendPort 发送消息给主 Isolate,如果主 Isolate 不想接受消息了,可以使用 receivePort.close(); 关闭管道。在管道关闭之前,新创建的Isolate 是可以一直给主 Isolate 发送消息的,只是上面只是发送了结果给主 Isolate 。