Dart SDK 安装
如果是使用Flutter 开发,Flutter SDK自带,无需单独安装Dart SDK。
如果需要独立调试运行Dart代码,需要独立安装Dart SDK。
官方文档:https://dart.dev/get-dart
-
windows(推荐): http://www.gekorm.com/dart-windows
-
mac: 如果mac电脑没有安装
brew
这个工具首先第一步需要安装它: https://brew.sh 然后执行brew tap dart-lang/dart
、brew install dart
Dart 开发工具:
- Dart的开发工具有很多: IntelliJ IDEA 、 WebStorm、 Atom、Vscode、Android Studio等
- 不使用IDE,在线运行Dart:https://dartpad.cn
如果在Vscode中配置Dart:
-
找到vscode插件安装dart
-
找到vscode插件安装code runner, Code Runner 可以运行我们的文件
基础
Dart是一个强类型语言,任何变量都是有确定类型的。
官方文档:Dart 编程语言概览
变量和命名
自动类型推断:
var name = 'lisa'; // name 变量的类型被推断为 String
在Dart中,当用var
声明一个变量后,Dart在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定。比如上面的name已经被确定为String类型,不能在给它赋值其他的类型,否则会报错。(这个行为和kotlin一致)
var name = 'lisa';
name = 123; // 报错
name = '张三'; // 不报错
尽量使用确定类型来声明变量
String name = '张三';
如果不知道类型可以使用dynamic
动态类型
dynamic name ="张三";
name = 666;
Dart的命名规则:
- 变量名称必须由数字、字母、下划线和美元符($)组成。
- 注意:标识符开头不能是数字
- 标识符不能是保留字和关键字。
- 变量的名字是区分大小写的如: age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量。
- 标识符(变量名称)一定要见名思意 :变量名称建议用名词,方法名称建议用动词
Dart 语言关键字列表:
在Dart中, 未初始化的变量默认值是 null
。 即使变量是数字类型默认值也是 null
,因为在 Dart 中一切都是对象,数字类型也不例外。
int? num;
print(num); //null
dart 入口函数:
// void 可以省略
void main(){
print('你好dart');
}
常量
使用过程中从来不会被修改的变量, 可以使用 final
或 const
, 而不是 var
或者其他类型。两者区别在于:const 变量是一个编译时常量,final变量在第一次使用时被初始化。使用final
或者const
修饰的变量,变量类型可以省略。
const
一开始就得赋值,final
可以开始不赋值 只能赋一次 ; 而final
不仅有const
的编译时常量的特性,最重要的它是运行时常量,并且final
是惰性初始化,即在运行时第一次使用前才初始化。
// 使用final定义变量。
final String name = '李四';
final int age = 24;
// 使用const定义变量。
const String gender = '男';
const int height = 165;
final name = 'Bob'; // Without a type annotation
const bar = 1000000;
相同的const
常量不会在内存中重复创建,但是final
会:
final finalList1 = [1, 2];
final finalList2 = [1, 2];
print(identical(finalList1, finalList2)); // false identical 用于检查两个引用是否指向同一个对象
const constList1 = [1, 2];
const constList2 = [1, 2];
print(identical(constList1, constList2)); // true
内建类型
Dart 语言支持以下内建类型:
- Number :
int
、double
- String:
String
- Boolean:
bool
- List (也被称为 Array)
- Map
- Set
- Rune (用于在字符串中表示 Unicode 字符)
- Symbol
这些类型都可以被初始化为字面量。
因为在 Dart 所有的变量终究是一个对象(一个类的实例), 所以变量可以使用 构造涵数 进行初始化。 一些内建类型拥有自己的构造函数。 例如, 通过 Map()
来构造一个 map 变量。
字符串
字符串定义的几种方式
var str1='this is str1';
var str2="this is str2";
String str1='this is str1';
String str2="this is str2";
String str1='''this is str1
this is str1
this is str1
''';
String str1="""
this is str1
this is str1
this is str1
""";
字符串的拼接
String str1='你好';
String str2='Dart';
print("$str1 $str2");
print(str1 + str2);
print(str1 +" "+ str2);
String转其他类型都是通过parse
方法:
String str = '123';
var myNum = int.parse(str);
print(myNum is int);
String str2 = '123.1';
var myNum2 = double.parse(str2);
print(myNum2 is double);
String price = '12';
var myNum3 = double.parse(price);
print(myNum3);
print(myNum3 is double);
其他类型转String
通过toString
方法
print(myNum3.toString());
int? bbb;
print(bbb.toString()); //正常 其他类型的null可以转成null字符串
String? ccc;
print(double.parse(ccc));//报错,null类型String不能转为其他类型
字符串分割
var list = str.split('-'); // 字符串分割转化成List
Runes
// 表示符文的意思,用于在字符串中表示Unicode字符。
// 使用String.fromCharCodes显示字符图形。如果非4个数值,需要把编码值放到大括号中。
Runes runes = Runes('\u{1f605} \u6211');
var str1 = String.fromCharCodes(runes);
print(str1);
List(数组/集合)
List中的对象可以各不相同,定义List的几种方式
// 1、第一种定义List的方式
var l1 = ["张三",20,true];
print(l1); //[张三, 20, true]
print(l1[0]); //张三
print(l1[1]); //20
print(l1.length); //3
// 2、第二种定义List的方式 指定类型
var l2 = <String>["张三","李四"];
var l3 = <int>[12, 30];
print(l2);
print(l3);
// 3、第三种定义List的方式 增加数据 ,通过[]创建的集合它的容量可以变化
var l4 = [];
print(l4);
print(l4.length);
l4.add("张三");
l4.add("李四");
l4.add(20);
print(l4);
print(l4.length);
var l5 = ["张三", 20, true];
l5.add("李四");
l5.add("zhaosi");
print(l5);
// 4、第四种定义List的方式,通过List.filled创建的集合长度是固定
var l6 = List.filled(2, "");
print(l6.length);
l6.length = 0; // 修改集合的长度 报错
var l7 = <String>["张三","李四"];
print(l7.length); //2
l7.length=0; //可以改变的
print(l7); //[]
var l8 = List<String>.filled(2, "");
l8[0]="string";
// l8[0]=222;
print(l8);
// 通过List.from创建
final numbers = <num>[1, 2, 3];
final listFrom = List<int>.from(numbers);
print(listFrom); // [1, 2, 3]
// 通过List.of创建
final numbers = <int>[1, 2, 3];
final listOf = List<num>.of(numbers);
print(listOf); // [1, 2, 3]
// 通过List.generate创建
final growableList =
List<int>.generate(3, (int index) => index * index, growable: true);
print(growableList); // [0, 1, 4]
final fixedLengthList =
List<int>.generate(3, (int index) => index * index, growable: false);
print(fixedLengthList); // [0, 1, 4]
// 通过List.unmodifiable创建
final numbers = <int>[1, 2, 3];
final unmodifiableList = List.unmodifiable(numbers); // [1, 2, 3]
unmodifiableList[1] = 87; // 报错 不可修改
List各种操作方法:
void listAdd() {
List<String> list = ["周一", "周二", "周三"];
list.add("周四");
list.add("周五");
print(list);
List<String> l1 = ["周一", "周二", "周三"];
List<String> l2 = ["周四", "周五", "周六", "周日"];
l1.addAll(l2);
print(l1); // [周一, 周二, 周三, 周四, 周五, 周六, 周日]
}
void listInsert() {
List<String> l1 = ["周一", "周二", "周三"];
l1.insert(3, "周四");
print(l1); // [周一, 周二, 周三, 周四]
l1.insert(0, "周日");
print(l1); // [周日, 周一, 周二, 周三, 周四]
}
void listInsert2() {
List<String> l1 = ["周一", "周二", "周三"];
List<String> l2 = ["周四", "周五", "周六", "周日"];
l1.insertAll(1, l2);
print(l1); // [周四, 周五, 周六, 周日, 周一, 周二, 周三]
}
void listRemove() {
List<String> l1 = ["周一", "周二", "周三", "周一"];
l1.remove("周一");
print(l1); // [周二, 周三, 周一]
l1.removeAt(1);
print(l1);
l1.removeLast();
print(l1);
// l1.clear();
}
//根据条件删除元素
void listRemoveWhere() {
List<String> l1 = ["周一", "周二", "周三"];
l1.removeWhere((e) => e == "周二");
print(l1); // [周一, 周三]
List<String> l2 = ["周一", "周二", "周三", "星期四"];
l2.removeWhere((e) => e.contains("周"));
print(l2); // [星期四]
List<String> l3 = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
l3.removeRange(2, 5); //包前不包后
print(l3); // [周一, 周二, 周六, 周日]
}
void listSet() {
List<String> l1 = ["周一", "周二", "周三", "星期四"];
l1[3] = "周四";
print(l1); // [周一, 周二, 周三, 周四]
List<String> l2 = ["周一", "周二", "周三", "周四", "周五"];
l2.fillRange(1, 4, "AllenSu"); //用相同的值替换指定索引范围内的所有元素(含头不含尾)
print(l2); // [周一, AllenSu, AllenSu, AllenSu, 周五]
List<String> l3 = ["周一", "周二", "周三"];
l3.replaceRange(1, 2, ["周四", "周五", "周六", "周日"]); // 用某一数组替换指定索引范围内的所有元素(含头不含尾)
print(l3); // [周一, 周四, 周五, 周六, 周日, 周三]
List<String> l4 = ["周一", "周二", "周三", "周四", "周五"];
l4.replaceRange(0, 4, ["周六", "周日"]);
print(l4); // [周六, 周日, 周五]
List<String> l5 = ["11", "22", "33"];
List<String> l6 = ["aa", "bb", "cc"];
l5.setRange(0, 2, l6); // 用 l6 数组中索引为 0 1 位置处元素的值,替换掉 l5 数组中索引 0 1 位置处元素的值
print(l5); // [aa, bb, 33]
List<String> l7 = ["周一", "周二", "周三", "周四"];
List<String> l8 = ["周五", "周六", "周日"];
l7.setAll(1,
l8); // 从指定索引位置开始,使用第二个数组内的元素依次替换掉第一个数组中的元素 注意: index + l2.length <= l1.length ,否则会报错。
print(l7); // [周一, 周五, 周六, 周日]
}
void listSearch() {
List<String> l1 = ["周一", "周二", "周三", "周四"];
String str = l1.elementAt(2);
print(str); // 周三
print(l1[2]); // 周三
print(l1.first); // 周一
print(l1.last); // 周四
List<String> l2 = ["周一", "周二", "周三", "周四", "周三"];
bool a = l2.contains("周一");
bool b = l2.contains("周日");
print(a); // true
print(b); // false
print(l2.indexOf("周三")); //2
print(l2.indexOf("周日")); //-1
print(l2.lastIndexOf("周三")); //4
print(l2.lastIndexOf("周日")); //-1
print(l2.indexWhere((e) => e == "周三")); //2 返回第一个满足条件的元素的索引
print(l2.lastIndexWhere((e) => e == "周三")); //4 从后往前找 返回第一个满足条件的元素的索引
List<int> l3 = [8, 12, 4, 1, 17, 33, 10];
Iterable<int> l4 =
l3.where((e) => e > 10); //根据条件函数筛选每个元素,符合条件的元素组成一个新的 Iterable
print(l4); // (12, 17, 33)
List<int> l5 = l3.where((e) => e >= 10).toList();
print(l5); // [12, 17, 33, 10]
print(l3.firstWhere((e) => e > 10)); // 12 返回第一个满足条件的元素
print(l3.lastWhere((e) => e < 10)); // 1 从后往前找 返回第一个满足条件的元素
int res1 = l3.singleWhere((e) => e > 30); //获取满足指定条件的唯一元素
int res2 = l3.singleWhere((e) => e > 33, orElse: () => -1);
// int res3 = l3.singleWhere((e) => e > 10, orElse: () => null);
print(res1); // 33
print(res2); // -1。数组中没有满足条件的元素,返回 orElse 方法指定的返回值(-1是自己填的,你也可以设置为其它值)
// print(res3); // 报错。当数组中有多个满足条件的元素时,即使你设置了 orElse 的返回值,用 singleWhere 也会报错
List<int> l6 = [8, 12, 4, 1, 17, 33, 10];
l6.retainWhere((e) => e > 10); // 保留满足条件的元素(改变了原数组)
print(l6); // [12, 17, 33]
List<int> l7 = [8, 12, 4, 1, 17, 33, 10];
print(l7.any((e) => e > 30)); //true 判断数组中是否有满足指定条件的元素
print(l7.every((e) => e > 5)); //false 判断数组中是否每个元素都满足指定的条件
List<int> l8 = [8, 12, 4, 1, 17, 33, 10];
Iterable<int> l9 = l8.take(3); // 从索引 0 位置处,取指定个数的元素
print(l9); // (8, 12, 4)
//从索引 0 位置处,查询满足指定条件的元素,直到出现第一个不符合条件的元素,然后返回前面符合条件的元素
Iterable<int> l10 = l8.takeWhile((e) => e > 1);
print(l10); // (8, 12, 4)
Iterable<int> l11 = l8.skip(4); // 跳过指定个数的元素,返回后面的元素
print(l11); // (17, 33, 10)
Iterable<int> l12 =
l8.skipWhile((e) => e < 17); //跳过满足指定条件的元素,直到不满足条件的元素,然后将该元素后面的所有元素返回
print(l12); // (17, 33, 10)
List<int> l13 = l8.sublist(5); //从指定索引处截取数组
print(l13); // [33, 10]
List<int> l14 = l8.sublist(2, 5);
print(l14); // [4, 1, 17] 含头不含尾
Iterable<int> l15 = l8.getRange(2, 5); //截取指定索引范围内的元素
print(l15); // (4, 1, 17)
List list = [
"a",
15,
"b",
false,
true,
20,
"c",
{"name": "AllenSu"}
];
Iterable<String> list1 = list.whereType(); // 从混合类型的数组中,筛选出指定类型的元素
Iterable<int> list2 = list.whereType();
Iterable<bool> list3 = list.whereType();
Iterable<Map> list4 = list.whereType();
print(list1); // (a, b, c)
print(list2); // (15, 20)
print(list3); // (false, true)
print(list4); // ({name: AllenSu})
}
void listOperate() {
List<int> l1 = [8, 12, 4, 6, 22, 35, 10];
l1.forEach((e) => print(e));
for(var i = 0; i < l1.length; i++) {
print(l1[i]);
}
for (var item in l1) {
print(item);
}
Iterable<bool> l2 =
l1.map((e) => e > 10); //遍历数组中的所有元素,可以对元素进行处理,并返回新的 Iterable
Iterable<String> l3 = l1.map((e) => e > 10 ? "大" : "小");
Iterable<num> l4 = l1.map((e) => e > 12 ? e / 2 : e * 2);
List<String> l5 = l1.map((e) => e % 2 == 0 ? "even" : "odd").toList();
print(l2); // (false, true, false, false, true, true, false)
print(l3); // (小, 大, 小, 小, 大, 大, 小)
print(l4); // (16, 24, 8, 12, 11.0, 17.5, 20)
print(l5); // [even, even, even, even, even, odd, even]
List<int> l7 = [8, 12, 8, 6, 22, 12, 10];
Set<int> l8 = l7.toSet(); //去重
print(l8); // {8, 12, 6, 22, 10}
List<String> l9 = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
Map map = l9.asMap(); // 将 List 转换为 Map,key 为原数组的索引,value 为原数组的元素
print(map); // {0: 周一, 1: 周二, 2: 周三, 3: 周四, 4: 周五, 5: 周六, 6: 周日}
List<int> sortList1 = [8, 12, 8, 6, 22, 12, 10];
sortList1.sort(); //排序
List<String> sortList2 = ["f", "d", "b", "c", "a", "e"];
sortList2.sort();
print(sortList1); // [6, 8, 8, 10, 12, 12, 22]
print(sortList2); // [a, b, c, d, e, f]
print(sortList2.reversed); //反序
List<int> list1 = [8, 12, 8, 6, 22, 12, 10];
String str1 = list1.join();
String str2 = list1.join("");
String str3 = list1.join(" ");
String str4 = list1.join(","); //用指定字符连接数组中每个元素,返回 String
String str5 = list1.join("-");
print(str1); // 81286221210
print(str2); // 81286221210
print(str3); // 8 12 8 6 22 12 10
print(str4); // 8,12,8,6,22,12,10
print(str5); // 8-12-8-6-22-12-10
List<int> intList = [8, 12, 8];
var anotherList = intList.cast(); // 指定 l2 的数据类型和 l1 的一样,都是 int 类型
anotherList.add(6);
print(intList); // [8, 12, 8, 6] 二者是同一个数组引用
// anotherList.add("ddd");
// print(intList); // 报错,提示无法将 String 类型的数添加到 int 类型的数组中
// generate 快速生产 Flutter 中的 Widget
// children: List.generate(l1.length, (index){
// return Text("$index");
// })
List<String> strList =
List.generate(l1.length, (index) => "$index ==> ${l1[index]}");
print(strList);
//reduce 用指定的函数方式对数组中的所有元素做连续操作,并将结果返回
List<int> list = [1, 2, 3, 4];
int res1 = list.reduce((a, b) => (a * b)); // 元素依次相乘
int res2 = list.reduce((a, b) => (a + b)); // 元素依次相加
print(res1); // 1*2*3*4 = 24
print(res2); // 1+2+3+4 = 10
//fold 根据一个现有数组和一个初始参数值 initValue,指定参数条件操作现有数组的所有元素,并返回处理的结果
// 2 为初始参数值,后面定义的方法为该初始值和原数组之间的处理方式
int res3 = list.fold(2, (a, b) => (a * b)); // 相乘
int res4 = list.fold(2, (a, b) => (a + b)); // 相加
print(res3); // 2*(1*2*3*4) = 48
print(res4); // 2+(1+2+3+4) = 12
// 将 l1 数组内的每一个元素都和指定的表达式组相操作
Iterable<int> a2 = list.expand((e) => [e + 1]);
Iterable<int> a3 = list.expand((e) => [e + 1, e + 2]);
Iterable<int> a4 = list.expand((e) => [e + 2, e * 2]);
Iterable<num> a5 = list.expand((e) => [e * 2, e / 2]);
Iterable<int> a6 = list.expand((e) => [e, e + 1, e + 2]);
print(a2); // (2, 3, 4, 5)
print(a3); // (2, 3, 3, 4, 4, 5, 5, 6)
print(a4); // (3, 2, 4, 4, 5, 6, 6, 8)
print(a5); // (2, 0.5, 4, 1.0, 6, 1.5, 8, 2.0)
print(a6); // (1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6)
list.shuffle(); //随机变换数组元素顺序
print(list); // [1, 4, 2, 3]
print(list.isEmpty);
print(list.isNotEmpty);
}
Map
定义 Map 的方式
//第一种定义Map的方式 有点像js定义json对象的方式
var person = {
"name": "张三",
"age": 20,
"work": ["程序员", "送外卖"]
};
print(person);
print(person["name"]);
print(person["age"]);
print(person["work"]);
//第二种定义Map的方式
var p = new Map();
p["name"] = "李四";
p["age"] = 22;
p["work"] = ["程序员", "送外卖"];
print(p);
print(p["age"]);
Map常用属性:
Map person = {
"name": "张三",
"age": 20,
"sex": "男"
};
print(person.keys.toList());
print(person.values.toList());
print(person.isEmpty);
print(person.isNotEmpty);
Map常用方法:
Map person = {
"name": "张三",
"age": 20,
"sex": "男"
};
// 合并映射 给映射内增加属性
person.addAll({ "work": ['敲代码', '送外卖'], "height": 160});
print(person);
person.remove("sex"); // 删除指定key的数据
print(person);
print(person.containsValue('张三')); // 查看映射内的值 返回true/false
person.forEach((key, value) {
print("$key---$value");
});
Set
它最主要的功能就是去除数组重复内容。Set是没有顺序且不能重复的集合,所以不能通过索引去获取值。
var s = new Set();
s.add('香蕉');
s.add('苹果');
s.add('苹果');
print(s); //{香蕉, 苹果}
print(s.toList());
List myList=['香蕉','苹果','西瓜','香蕉','苹果','香蕉','苹果'];
var s = new Set();
s.addAll(myList);
print(s);
print(s.toList());
s.forEach((value)=>print(value));
// set1.difference(set2):返回set1集合里有但set2里没有的元素集合
// set1.intersection(set2):返回set1和set2的交集
// set1.union(set2):返回set1和set2的并集
// set1.retainAll():set1只保留某些元素(要保留的元素要在原set中存在)
类型判断
var str=123;
if (str is String) {
print('是string类型');
} else if(str is int) {
print('int');
} else {
print('其他类型');
}
Person p = Person('张三', 20);
if (p is Person) {
p.name = "李四";
}
p.printInfo();
print(p is Object); //true
强制转换类型:
var p1;
p1 = '';
p1 = new Person('张三1', 20);
p1.printInfo();
(p1 as Person).printInfo(); // 强转
运算符和操作符
算术运算符:
Dart中的除是真的除,跟java的取整不一样
int a = 13;
int b = 5;
print(a + b); // 加
print(a - b); // 减
print(a * b); // 乘
print(a / b); // 2.6 除
print(a ~/ b); // 2 取整
print(a % b); // 3 取余
??
赋值运算符:
bool? b;
b ??= false; // 如果b为空时,将false赋值给b,否则,b的值保持不变。
print(b);
// a ?? b
// 如果 a != null,返回 a 的值; 否则返回 b 的值。
// 如果赋值是基于判定是否为 null, 考虑使用 ??。
String playerName = p.name ?? 'Guest'; // 等价于:name != null ? name : 'Guest';
复合赋值运算符: +=
-=
*=
/=
%=
~/=
使用同java一样
关系运算符:==
!=
>
<
>=
<=
使用同java一样
逻辑运算符:||
&&
!
使用同java一样
条件表达式:if-else
、switch-case
、三目运算符 c = flg ?a :b
使用同java一样
自增自减运算符: ++
--
使用同java一样
?.
非空操作符:
Person p = Person('张三', 20);
// p不为null才执行右边的
p?.printInfo();
..
级联操作符:
Person p1 = new Person('张三1', 20)
..name = "李四"
..age = 30
..printInfo();
// 上面等价下面的
// Person p1 = new Person('张三1', 20);
// p1.printInfo();
// p1.name='张三222';
// p1.age=40;
// p1.printInfo();
函数
定义一个方法然后打印用户信息:
String printUserInfo(String username, int age) { //行参
return "姓名:$username---年龄:$age";
}
print(printUserInfo('张三', 20)); //实参
定义一个带可选参数或默认参数值的方法(可选参数使用[]
指定) ,最新的dart定义可选参数需要指定类型默认值:
String printUserInfo(String username,[String sex = '男',int age = 0]) {
if (age != 0) {
return "姓名:$username---性别:$sex--年龄:$age";
}
return "姓名:$username---性别:$sex--年龄保密";
}
print(printUserInfo('张三'));
print(printUserInfo('小李','女'));
print(printUserInfo('小李','女',30));
定义一个命名参数的方法(命名参数使用{}
指定),最新的dart定义命名参数需要指定类型默认值:
String printUserInfo(String username, {int age = 0, String sex = '男'}) {
return "姓名:$username-性别:$sex-年龄:${age}";
}
print(printUserInfo('张三', age: 20, sex: '女'));
定义一个把方法当做函数参数的方法:
//fn1方法
fn1() {
print('fn1');
}
//fn2方法
fn2(fn) {
fn();
}
//调用fn2这个方法 把fn1这个方法当做参数传入
fn2(fn1);
匿名方法:
var printNum = (int n, int m) => print(m * n); // 匿名方法
printNum(1, 2); // 调用
var printNum2 = () => print("aaaaa"); // 无参匿名函数
printNum2 (); // 调用
定义一个返回函数类型的函数:
/// 返回一个函数,返回的函数参数与 [addBy] 相加。
Function makeAdder(num addBy) {
return (num i) => addBy + i;
}
void testMakeAdder() {
// 创建一个加 2 的函数。
var add2 = makeAdder(2);
// 创建一个加 4 的函数。
var add4 = makeAdder(4);
print(add2(3) == 5); //true
print(add4(3) == 7); //true
}
箭头函数:
// 需求:使用forEach打印下面List里面的数据
List list = ['苹果','香蕉','西瓜'];
list.forEach((value){
print(value);
});
// 注意和方法的区别: 箭头函数内只能写一条语句,并且语句后面没有分号(;)
list.forEach((value) => print(value) );
// 需求:修改下面List里面的数据,让数组中大于2的值乘以2
List list = [4,1,2,3,4];
var newList = list.map((value) {
if (value > 2) {
return value * 2;
}
return value;
});
print(newList.toList());
var newList = list.map((value) => value> 2 ? value * 2 : value);
print(newList.toList());
函数别名:
//函数别名 使用typedef给函数定义一个名字代替使用
typedef Fun1(int a, int b);
typedef Fun2<T, K>(T a, K b);
int add(int a, int b) {
return a + b;
}
class Demo1 {
Demo1(int f(int a, int b), int x, int y) {
var sum = f(x, y);
print("sum1 = $sum");
}
}
class Demo2 {
Demo2(Fun1 f, int x, int y) {
var sum = f(x, y);
print("sum2 = $sum");
}
}
class Demo3 {
Demo3(Fun2<int, int> f, int x, int y) {
var sum = f(x, y);
print("sum3 = $sum");
}
}
void main() {
Demo2 demo2 = Demo2(add, 2, 3);
Demo3 demo3 = Demo3(add, 4, 5);
}
闭包:
- 全局变量特点: 全局变量常驻内存、污染全局
- 局部变量的特点:不常驻内存会被垃圾机制回收、不会污染全局
闭包解决的问题:
- 常驻内存
- 不污染全局
闭包的本质: 函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)
闭包的写法:函数嵌套函数,并return 里面的函数,这样就形成了闭包。
void main() {
fn() {
var a = 123; /*不会污染全局 常驻内存*/
return () {
a++;
print(a);
};
}
var b = fn();
b();
b();
b();
// print(a); // 其他地方访问不到a
}
Null safety
Null safety翻译成中文的意思是空安全。null safety 可以帮助开发者避免一些日常开发中很难被发现的错误,并且额外的好处是可以改善性能。
Flutter2.2.0(2021年5月19日发布) 之后的版本都要求使用null safety。
dart用 ?
表示可空类型,类型后面带 ?
表示可空类型,不带表示不可空类型。这点跟kotlin一样。
null
不能被赋值给非空类型
String username = "张三";
username = null; // 报错
List<String> l1 = ["张三","李四","王五"];
l1 = null; // 报错
String? username = "张三"; // String? 表示username是一个可空类型
username = null; // ok
print(username);
int? a = 123; // int? 表示a是一个可空类型
a = null; // ok
print(a);
List<String>? l1 = ["张三","李四","王五"];
l1 = null; // ok
print(l1);
String? getData(apiUrl){
if(apiUrl!=null){
return "this is server data";
}
return null;
}
//调用方法
print(getData("http://www.itying.com"));
print(getData(null));
dart用 !
表示类型断言
void printLength(String? str){
try {
//类型断言: 如果str不等于null 会打印str的长度,如果等于null会抛出异常
print(str!.length);
} catch (e) {
print("str is null");
}
}
printLength("str");
printLength(null);
late 延迟初始化
跟kotlin的 lateinit 很像,不过kotlin的 lateinit 不能修饰基本类型,而dart可以。
class Person {
late String name;
late int age;
void setName(String name, int age) {
this.name = name;
this.age = age;
}
String getName() {
return "${this.name}---${this.age}";
}
}
void main(args) {
Person p = new Person();
p.setName("张三", 20);
print(p.getName());
}
required
required
翻译成中文的意思是需要、依赖
required
关键词: 最开始 @required
是注解,现在它已经作为内置修饰符。
主要用于允许根据需要标记任何命名参数(函数或类),使得它们不为空。因为可选参数中必须有个 required 参数或者该参数有个默认值。
String printUserInfo(String username, {int age=10, String sex="男"}) {
return "姓名:$username---性别:$sex--年龄:$age";
}
String printInfo(String username, {required int age, required String sex}) {
return "姓名:$username---性别:$sex--年龄:$age";
}
void main(args) {
// 不加required的命名参数,可以不传,可以只传部分
print(printUserInfo('张三'));
print(printUserInfo('张三', age: 20));
//加required:age 和 sex必须传入
print(printInfo('张三',age: 22,sex: "女"));
}
在类的构造函数参数中使用required
(Flutter组件中构造函数经常这样写):
class Person {
String name;
int age;
// {}表示 name 和age 是必须传入的命名参数
// required表示 name 和age 必须传入
Person({required this.name, required this.age});
String getName() {
return "${this.name}---${this.age}";
}
}
void main(args) {
Person p = new Person(name: "张三", age: 20);
print(p.getName());
}
class Person {
String? name; //可空属性
int age;
Person({this.name, required this.age}); //可空的参数不用加required
String getName() {
return "${this.name}---${this.age}";
}
}
void main(args) {
Person p = new Person(age: 20);
print(p.getName()); //张三---20
Person p1 = new Person(age: 20);
print(p1.getName()); //null---20
}
面向对象
Dart所有的东西都是对象,所有的对象都继承自Object类。
类
Dart是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类
一个类通常由属性和方法组成。
class Person{
String name = "张三";
int age = 23;
void getInfo() {
// print("$name----$age");
print("${this.name}----${this.age}");
}
void setInfo(int age) {
this.age=age;
}
}
void main(){
//实例化
Person p1=new Person();
p1.setInfo(28);
p1.getInfo();
print(p1.name);
p1.getInfo();
}
构造函数
默认无参构造函数:
class Person{
String name='张三';
int age=20;
//默认构造函数
Person(){
print('这是构造函数里面的内容 这个方法在实例化的时候触发');
}
void printInfo(){
print("${this.name}----${this.age}");
}
}
普通构造函数:
// 注意:最新版本的dart中需要初始化不可为null的实例字段,如果不初始化的话需要在属性前面加上late
class Person{
late String name;
late int age;
Person(String name,int age){
this.name=name;
this.age=age;
}
void printInfo(){
print("${this.name}----${this.age}");
}
}
构造函数的简写:
class Person{
late String name;
late int age;
Person(this.name,this.age);
void printInfo(){
print("${this.name}----${this.age}");
}
}
class Container{
int width;
int height;
Container({required this.width, required this.height});
}
void main(){
var c1 = new Container(width: 100,height: 100);
var c2 = new Container(width: 100,height: 100);
}
命名构造函数:
class Person {
late String name;
late int age;
//构造函数的简写
Person(this.name, this.age);
Person.now() {
print('我是命名构造函数');
}
//命名构造函数传参
Person.setInfo(String name, int age) {
this.name = name;
this.age = age;
}
void printInfo() {
print("${this.name}----${this.age}");
}
}
void main() {
Person p1 = new Person('张三', 20); //默认实例化类的时候调用的是 默认构造函数
var d = new DateTime.now(); //实例化DateTime调用它的命名构造函数
print(d);
Person p1 = new Person.now(); //调用命名构造函数
Person p1 = new Person.setInfo('李四', 30); //调用命名构造函数
p1.printInfo();
}
dart里面构造函数可以写多个。
工厂构造函数:
工厂构造函数是一种构造函数,与普通构造函数不同,工厂函数不会自动生成实例,而是通过代码来决定返回的实例对象。
如果一个构造函数并不总是返回一个新的对象(单例),则使用 factory
来定义这个构造函数。工厂构造函数无法访问this
。
class Singleton {
String name;
//工厂构造函数无法访问this,所以这里要用static
static Singleton? _cache;
//工厂方法构造函数,关键字factory
factory Singleton([String name = 'singleton']) =>
Singleton._cache ??= Singleton._newObject(name);
//定义一个命名构造函数用来生产实例
Singleton._newObject(this.name);
}
构造函数的初始化列表
Dart中我们也可以在构造函数体运行之前初始化实例变量
class Rect {
int height;
int width;
Rect(): height = 2, width = 10 {
print("${this.height}---${this.width}");
}
getArea() {
return this.height * this.width;
}
}
void main() {
Rect r = new Rect();
print(r.getArea());
}
class Point {
//final变量不能被修改,必须被构造函数初始化
final num x;
final num y;
final num distanceFromOrigin;
//初始化列表构造函数
Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}
导入类
可以把类单独放一个.dart
文件中,在其他文件中导入使用
import 'lib/Person.dart';
void main(){
Person p1=new Person.setInfo('李四1',30);
p1.printInfo();
}
私有方法 和 私有属性
Dart和其他面向对象语言不一样,Data中没有 public private protected
这些访问修饰符
但是我们可以使用下划线_
把一个属性或者方法定义成私有。
// Animal.dart
class Animal{
late String _name; //私有属性
late int age;
//默认构造函数的简写
Animal(this._name,this.age);
void printInfo(){
print("${this._name}----${this.age}");
}
String getName(){
return this._name;
}
void _run(){
print('这是一个私有方法');
}
execRun(){
this._run(); //类里面方法的相互调用
}
}
import 'lib/Animal.dart';
void main() {
Animal a=new Animal('小狗', 3);
print(a.getName());
a.execRun(); //间接的调用私有方法
// a._run(); // 访问不到私有方法
// print(a._name); // 访问不到私有成员
print(a.age);
}
getter和setter
// 常规方式
class Rect {
num height;
num width;
Rect(this.height, this.width);
area() {
return this.height * this.width;
}
}
void main() {
Rect r = new Rect(10,4);
print("面积:${r.area()}");
}
// getter和setter 方式
class Rect {
late num height;
late num width;
Rect(this.height, this.width);
get area {
return this.height * this.width;
}
set areaHeight(value) {
this.height = value;
}
}
void main() {
Rect r=new Rect(10,4);
//注意调用直接通过访问属性的方式访问area
r.areaHeight = 6;
print("面积:${r.area}");
}
静态成员和静态方法
使用上跟java没差别
class Person {
static String name = '张三';
static void show() {
print(name);
}
}
main(){
print(Person.name);
Person.show();
}
class Person {
static String name = '张三';
int age=20;
static void show() {
print(name);
}
void printInfo(){ /*非静态方法可以访问静态成员以及非静态成员*/
// print(name); //访问静态属性
// print(this.age); //访问非静态属性
show(); //调用静态方法
}
static void printUserInfo(){//静态方法
print(name); //静态属性
show(); //静态方法
//print(this.age); //静态方法没法访问非静态的属性
// this.printInfo(); //静态方法没法访问非静态的方法
// printInfo();
}
}
main(){
// print(Person.name);
// Person.show();
// Person p=new Person();
// p.printInfo();
Person.printUserInfo();
}
继承
面向对象的三大特性:封装 、继承、多态
Dart中的类的继承:
- 1、子类使用extends关键词来继承父类
- 2、子类会继承父类里面可见的属性和方法 但是不会继承构造函数
- 3、子类能复写父类的方法 getter和setter
class Person {
String name='张三';
num age=20;
void printInfo() {
print("${this.name}---${this.age}");
}
}
class Man extends Person {
}
main() {
Man m = new Man();
print(m.name);
m.printInfo();
}
调用父类构造函数:
class Person {
late String name;
late num age;
Person(this.name,this.age);
void printInfo() {
print("${this.name}---${this.age}");
}
}
class Man extends Person {
Man(String name, num age) : super(name, age) {
}
}
class Person {
String name;
num age;
Person(this.name, this.age);
Person.from(this.name, this.age);
void printInfo() {
print("${this.name}---${this.age}");
}
}
class Man extends Person {
late String sex;
// 调用父类命名构造函数
Man(String name, num age, String sex) : super.from(name, age) {
this.sex = sex;
}
run() {
print("${this.name}---${this.age}--${this.sex}");
}
}
main() {
Man m = new Man('张三', 12, "男");
m.printInfo();
m.run();
}
覆写父类的方法:
class Person {
String name;
num age;
Person(this.name,this.age);
void printInfo() {
print("${this.name}---${this.age}");
}
work(){
print("${this.name}在工作...");
}
}
class Man extends Person{
Man(String name, num age) : super(name, age);
run(){
print('run');
}
// 可以写也可以不写 建议在覆写父类方法的时候加上 @override
void printInfo(){
print("姓名:${this.name}---年龄:${this.age}");
}
work(){
print("${this.name}的工作是写代码");
}
}
main(){
Man m = new Man('李四',20);
m.printInfo();
m.work();
}
子类调用父类的方法:
class Person {
String name;
num age;
Person(this.name,this.age);
void printInfo() {
print("${this.name}---${this.age}");
}
work(){
print("${this.name}在工作...");
}
}
class Man extends Person {
Man(String name, num age) : super(name, age);
run(){
print('run');
super.work(); //子类调用父类的方法
}
}
main(){
Man m = new Man('李四',20);
m.run();
}
抽象类
Dart抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口。
-
1、抽象类通过
abstract
关键字来定义 -
2、Dart中的抽象方法不能用
abstract
声明,Dart中没有方法体的方法我们称为抽象方法。 -
3、如果子类继承抽象类必须得实现里面的抽象方法
-
4、如果把抽象类当做接口实现的话必须得实现抽象类里面定义的所有属性和方法。
-
5、抽象类不能被实例化,只有继承它的子类可以
extends
抽象类 和 implements
的区别:
-
1、如果要复用抽象类里面的方法,并且要用抽象方法约束自类的话我们就用
extends
继承抽象类 -
2、如果只是把抽象类当做标准的话我们就用
implements
实现抽象类
案例:定义一个Animal
类要求它的子类必须包含eat
方法
abstract class Animal {
eat(); //抽象方法
run(); //抽象方法
printInfo(){
print('我是一个抽象类里面的普通方法');
}
}
class Dog extends Animal{
eat() {
print('小狗在吃骨头');
}
run() {
// TODO: implement run
print('小狗在跑');
}
}
class Cat extends Animal{
eat() {
// TODO: implement eat
print('小猫在吃老鼠');
}
run() {
// TODO: implement run
print('小猫在跑');
}
}
main(){
Dog d=new Dog();
d.eat();
d.printInfo();
Cat c=new Cat();
c.eat();
c.printInfo();
// Animal a=new Animal(); //抽象类没法直接被实例化
}
多态
与java一样,允许使用父类型的引用去接收子类型的实例, 多态就是父类先定义一个方法但不实现,让子类去实现,不同的子类有不同的实现,因此父类型引用的函数调用在运行时会有不同的执行效果,具体由运行时实际指向的子类决定其行为 。
abstract class Animal{
eat(); //抽象方法
}
class Dog extends Animal{
eat() {
print('小狗在吃骨头');
}
run(){
print('run');
}
}
class Cat extends Animal{
eat() {
print('小猫在吃老鼠');
}
run(){
print('run');
}
}
main(){
Animal d = new Dog();
d.eat();
Animal c = new Cat();
c.eat();
}
接口
和Java一样,dart也有接口,但是和Java还是有区别的。
-
首先,dart的接口没有
interface
关键字定义接口,而是普通类或抽象类都可以作为接口被实现。 -
同样使用
implements
关键字进行实现。
但是dart的接口有点奇怪,如果实现的类是普通类,会将普通类和抽象中的属性的方法全部需要覆写一遍。
而因为抽象类可以定义抽象方法,普通类不可以,所以一般如果要实现像Java接口那样的方式,一般会使用抽象类。 建议使用抽象类定义接口。
/**
* 定义一个DB库 支持 mysql mssql mongodb
* mysql mssql mongodb三个类里面都有同样的方法
*/
abstract class Db { //当做接口 接口:就是约定 、规范
late String uri; //数据库的链接地址
add(String data);
save();
delete();
}
class Mysql implements Db {
String uri;
Mysql(this.uri);
add(data) {
print('这是mysql的add方法'+data);
}
delete() {
return null;
}
save() {
return null;
}
remove(){
}
}
class MsSql implements Db {
late String uri;
add(String data) {
print('这是mssql的add方法'+data);
}
delete() {
return null;
}
save() {
return null;
}
}
main() {
Mysql mysql=new Mysql('xxxxxx');
mysql.add('1243214');
}
Dart中一个类实现多个接口:
abstract class A {
late String name;
printA();
}
abstract class B {
printB();
}
class C implements A, B {
late String name;
printA() {
print('printA');
}
printB() {
return null;
}
}
void main(){
C c = new C();
c.printA();
}
将普通类当作接口使用:
class D {
printD() {
print("aa");
}
}
// dart中的implements可以实现普通类,它把普通类的方法当做实现接口
class C implements D {
printD() {
}
}
mixin混入
mixins的中文意思是混入,就是在类中混入其他功能。
在Dart中可以使用mixins实现类似多继承的功能
因为mixins使用的条件(可能随着Dart版本更新而变化):
- 作为mixins的类必须使用mixin修饰符
- 作为mixins的类只能继承自Object,不能继承其他类
- 作为mixins的类不能有构造函数
- 一个类可以mixins多个mixins类
- mixins绝不是继承,也不是接口,而是一种全新的特性
mixin class A {
String info="this is A";
void printA(){
print("A");
}
}
mixin class B {
void printB(){
print("B");
}
}
class C with A,B {
}
void main(){
var c = new C();
c.printA();
c.printB();
print(c.info);
// 下面三个都是true说明c是A和B的子类型,效果上是多继承
print(c is C); //true
print(c is A); //true
print(c is B); //true
}
也可以直接这样写:
mixin A {
String info="this is A";
}
class C with A {
}
同时使用extends
和with
实现多继承:
class Person{
String name;
num age;
Person(this.name,this.age);
printInfo(){
print('${this.name}----${this.age}');
}
void run(){
print("Person Run");
}
}
mixin class A {
String info="this is A";
void printA(){
print("A");
}
void run(){
print("A Run");
}
}
mixin class B {
void printB(){
print("B");
}
void run(){
print("B Run");
}
}
class C extends Person with B,A {
C(String name, num age) : super(name, age);
}
void main(){
var c=new C('张三',20);
c.printInfo();
// c.printB();
// print(c.info);
c.run();
}
泛型
//只能返回string类型的数据
String getData(String value){
return value;
}
//同时支持返回 string类型 和int类型 (代码冗余)
String getData1(String value){
return value;
}
int getData2(int value){
return value;
}
// 同时返回 string类型 和number类型 不指定类型可以解决这个问题
getData(value){
return value;
}
不指定类型可以解决这个问题
// 同时返回 string类型 和number类型
getData(value){
return value;
}
不指定类型放弃了类型检查。我们现在想实现的是传入什么 返回什么。比如:传入number 类型必须返回number类型 传入 string类型必须返回string类型
T getData<T>(T value) {
return value;
}
void main(){
getData<String>('你好');
print(getData<int>(12));
}
基本跟java差不多。
集合 List
泛型类的用法:
//案例:把下面类转换成泛型类,要求MyList里面可以增加int类型的数据,也可以增加String类型的数据。
// 但是每次调用增加的类型要统一
/**
class MyList {
List list = <int>[];
void add(int value) {
this.list.add(value);
}
List getList() {
return list;
}
}
*/
class MyList<T> {
List list = <T>[];
void add(T value) {
this.list.add(value);
}
List getList() {
return list;
}
}
main() {
MyList l1 = new MyList(); // 不指定具体类型时,可以任何类型
l1.add("张三");
l1.add(12);
l1.add(true);
print(l1.getList());
MyList l2 = new MyList<String>(); // 指定泛型为String类型
l2.add("张三1");
// l2.add(11); //错误的写法 会报错
print(l2.getList());
MyList l3 = new MyList<int>(); // 指定泛型为int类型
l3.add(11);
l3.add(12);
// l3.add("aaaa"); //错误的写法 会报错
print(l3.getList());
}
系统内置List泛型用法:
List list = List.filled(2, "");
list[0] = "张三";
list[1] = "李四";
print(list);
List list = List<String>.filled(2, "");
list[0] = "张三1111";
list[1] = "李四";
print(list);
List list2 = List<int>.filled(2, 0);
list2[0] = 12;
list2[1] = 13;
print(list2);
泛型接口:
/**
Dart中的泛型接口:
实现数据缓存的功能:有文件缓存、和内存缓存。内存缓存和文件缓存按照接口约束实现。
1、定义一个泛型接口 约束实现它的子类必须有getByKey(key) 和 setByKey(key,value)
2、要求setByKey的时候的value的类型和实例化子类的时候指定的类型一致
*/
abstract class Cache<T> {
getByKey(String key);
void setByKey(String key, T value);
}
class FileCache<T> implements Cache<T> {
getByKey(String key) {
return null;
}
void setByKey(String key, T value) {
print("我是文件缓存 把key=${key} value=${value}的数据写入到了文件中");
}
}
class MemoryCache<T> implements Cache<T> {
getByKey(String key) {
return null;
}
void setByKey(String key, T value) {
print("我是内存缓存 把key=${key} value=${value} -写入到了内存中");
}
}
void main() {
MemoryCache m1 = new MemoryCache<String>();
m1.setByKey('index', '首页数据');
MemoryCache m = new MemoryCache<Map>();
m.setByKey('index', {"name": "张三", "age": 20});
}
导入library库
前面介绍Dart基础知识的时候基本上都是在一个文件里面编写Dart代码的,但实际开发中不可能这么写,模块化很重要,所以这就需要使用到库的概念。
在Dart中,库的使用时通过import
关键字引入的。
library
指令可以创建一个库,每个Dart文件都是一个库,即使没有使用library
指令来指定。
Dart中的库主要有三种:
- 我们自定义的库
import 'lib/xxx.dart';
- 系统内置库
import 'dart:math';
import 'dart:io';
import 'dart:convert';
- Pub包管理系统中的库
https://pub.dev/packages
https://pub.flutter-io.cn/packages
https://pub.dartlang.org/flutter/- 1、需要在自己想项目根目录新建一个pubspec.yaml
- 2、在pubspec.yaml文件 然后配置名称 、描述、依赖等信息
- 3、然后运行 pub get 获取包下载到本地
- 4、项目中引入库 import ‘package:http/http.dart’ as http; 看文档使用
// Dart中导入自己本地库
import 'lib/Animal.dart';
main(){
var a=new Animal('小黑狗', 20);
print(a.getName());
}
// 导入系统内置库
// import 'dart:io';
import "dart:math";
main(){
print(min(12,23));
print(max(12,25));
}
// 导入系统内置库实现请求数据httpClient
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();
}
导入库命名冲突解决
当引入两个库中有相同名称标识符的时候,如果是java通常我们通过写上完整的包名路径来指定使用的具体标识符,甚至不用import都可以,但是Dart里面是必须import
的。当冲突的时候,可以使用as
关键字来指定库的前缀。如下例子所示:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
Element element1 = new Element(); // Uses Element from lib1.
lib2.Element element2 = new lib2.Element(); // Uses Element from lib2.
部分导入
如果只需要导入库的一部分,有两种模式:
模式一:只导入需要的部分,使用show
关键字,如下例子所示:
import 'package:lib1/lib1.dart' show foo;
模式二:隐藏不需要的部分,使用hide
关键字,如下例子所示:
import 'package:lib2/lib2.dart' hide foo;
在Java编码过程经常会出现一个类中调用两个类中相同名字的方法,这时就需要全限定名称来调用了,但是在Dart中是不需要的, 导入时使用库前缀即可。
import 'MyLib1.dart' as lib1;
import 'MyLib2.dart' as lib2 hide test;
import 'MyLib3.dart' as lib3 show test;
void main() {
var compute = lib1.compute();
var compute2 = lib2.compute();
var test = lib3.test();
}
拆分库到多个文件中
// mylib.dart
library mylib;
//使用part 把一个库分开到多个 Dart 文件中。
part 'util.dart';
part 'tool.dart';
void printMyLib() => print('mylib');
// util.dart
part of mylib;
void printUtil() => print('util');
// tool.dart
part of mylib;
void printTool() => print('tool');
延迟加载
也称为懒加载,可以在需要的时候再进行加载。懒加载的最大好处是可以减少APP的启动时间。
懒加载使用deferred as
关键字来指定,如下例子所示:
import 'package:deferred/hello.dart' deferred as hello;
当需要使用的时候,需要使用loadLibrary()
方法来加载:
greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
identical
dart:core
库中identical
函数的用法: bool identical(Object? a, Object? b)
检查两个引用是否指向同一个对象。
void main() {
var o1 = new Object();
var o2 = new Object();
print(identical(o1, o2)); // alse, different objects.
print(identical(o1, o1)); // true, same object
//表示实例化常量构造函数
//o1 和 o2共享了存储空间
var o1 = const Object();
var o2 = const Object();
print(identical(o1, o2)); //true 共享存储空间
print(identical(o1, o1)); //true 共享存储空间
print(identical([2],[2])); //false 不共享存储空间
print(identical(const [2],const [2])); //true
print(identical(const [1],const [2])); //false
print(identical(2, 1 + 1)); // true
}
发现:const
关键词在多个地方创建相同的对象的时候,内存中只保留了一个对象
共享存储空间条件:1、常量, 2、值相等
//常量构造函数
class Container{
final int width;
final int height;
const Container({required this.width,required this.height});
}
// 实例化常量构造函数的时候,多个地方创建这个对象,如果传入的值相同,只会保留一个对象。
void main(){
var c1 = Container(width: 100, height: 100);
var c2 = Container(width: 100, height: 100);
print(identical(c1, c2)); //false
var c3 = const Container(width: 100, height: 100);
var c4 = const Container(width: 100, height: 100);
print(identical(c3, c4)); //true
var c5 = const Container(width: 100, height: 110);
var c6 = const Container(width: 120, height: 100);
print(identical(c5, c6)); //false
}
Dart异步编程
为什么异步代码很重要
异步操作让您的程序在等待另一个操作完成时完成工作。下面是一些常见的异步操作:
- 通过网络获取数据。
- 写入数据库。
- 从文件中读取数据。
这种异步计算通常以 Future
的形式提供它们的结果,如果结果有多个部分,则以Stream
形式。这些计算将异步引入程序。为了适应最初的异步,其他普通的 Dart 函数也需要变成异步的。
要与这些异步结果进行交互,您可以使用async
和await
关键字。大多数异步函数只是异步 Dart 函数,它们可能深深地依赖于固有的异步计算。
Future
Future 表示异步操作的结果,等价于JS的Promise
,Future有两种状态:未完成或已完成。
- 未完成:当您调用异步函数时,它会返回一个未完成的
Future
。这个Future
正在等待函数的异步操作完成或抛出错误。 - 已完成:如果异步操作成功,
Future
将返回一个值。否则,它将以错误完成。
返回一个值:Future<T>
以 T
类型的值完成。例如,Future<String>
会产生一个字符串值。如果Future不产生可用的价值,那么Future的类型是Future<void>
。
完成时出现错误:如果函数执行的异步操作因任何原因失败,Future将以错误完成。
// 2s后打印,不返回任何值
Future.delayed(Duration(seconds: 2),() => "hi world");
// 返回一个值
Future.delayed(Duration(seconds: 2), () {
return "hi world!";
}).then((data) {
print(data);
});
// 返回一个异常
Future.delayed(Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
//执行成功会走到这里
print("success");
}).catchError((e) {
//执行失败会走到这里
print(e);
});
// 同上,另一种写法
Future.delayed(new Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
whenComplete:无论成功或失败都会执行
Future.delayed(new Duration(seconds: 2), () {
return "hi world!";
}).then((data) {
//执行成功会走到这里
print(data);
}).catchError((e) {
//执行失败会走到这里
print(e);
}).whenComplete(() {
//无论成功或失败都会走到这里
print("Complete");
});
wait:等待多个异步任务都执行结束后再执行
Future.wait([
// 2秒后返回结果
Future.delayed(new Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(new Duration(seconds: 4), () {
return " world";
})
]).then((results) {
print(results[0] + results[1]);
}).catchError((e) {
print(e);
});
async 和 await
这两个用法与JS完全一致。
只需要记住两点:
- 要定义异步函数,请在函数体之前添加
async
关键字 await
关键字只能出现在async
修饰的方法中
async
是让方法变成异步。await
是等待异步方法执行完成。即将异步代码同步化。
定义异步方法
foo() async {
return '假设这里是某种异步任务';
}
使用 await
调用异步方法
void main() async {
var result = await foo();
print(result);
}
如果函数有声明的返回类型,则将类型更新为Future<T>
,其中T
是函数返回值的类型。如果函数没有显式返回值,则返回类型为Future<void>
:
Future<void> foo() async { ··· }
一个错误的异步代码示例:
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() = Future.delayed(const Duration(seconds: 2), () => 'Large Latte');
void main() {
print('Fetching user order...');
print(createOrderMessage());
}
以下是该示例无法打印最终生成的值的原因:
fetchUserOrder()
是一个异步函数,它在延迟后提供一个描述用户订单的字符串:“Large Latte”。- 要获得用户的订单,
createOrderMessage()
应该调用fetchUserOrder()
并等待它完成。因为createOrderMessage()
没有等待fetchUserOrder()
完成,createOrderMessage()
未能得到fetchUserOrder()
最终提供的字符串值。 - 相反,
createOrderMessage()
获取待完成工作的表示:未完成的Future。 - 因为
createOrderMessage()
无法获取描述用户订单的值,该示例无法将“Large Latte”
打印到控制台,而是打印“Your order is: Instance of '_Future<String>'”
。
正确写法是使用 async
和 await
:
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() = Future.delayed(const Duration(seconds: 2), () => 'Large Latte');
Future<void> main() async {
print('Fetching user order...');
print(await createOrderMessage());
}
使用多个await
保证多个异步任务按顺序执行:
task() async {
try {
String id = await login("alice", "******");
String userInfo = await getUserInfo(id);
await saveUserInfo(userInfo);
//执行接下来的操作
} catch (e) {
//错误处理
print(e);
}
}
getUserInfo(String id) async {
return Future.delayed(Duration(seconds: 2),() => "hi world");
}
saveUserInfo(String id) async {
return Future.delayed(Duration(seconds: 2),() => "hi world");
}
login(String s, String t) async {
return Future.delayed(Duration(seconds: 2), () => "hi world");
}
在一个async函数体内,await之前的代码会同步立即执行,await之后的代码会被阻塞直到await返回。
处理错误: 要处理async
函数中的错误,请使用 try-catch
try {
var order = await fetchUserOrder();
} catch (err) {
print('Caught error: $err');
}
Future<void> printOrderMessage() async {
try {
print('Awaiting user order...');
var order = await fetchUserOrder();
print(order);
} catch (err) {
print('Caught error: $err');
}
}
Future<String> fetchUserOrder() = Future.delayed(const Duration(seconds: 2), () => 'Large Latte');
void main() async {
await printOrderMessage();
}
Stream
Future 和 Stream 类是 Dart 异步编程的核心。
- Future 表示一个不会立即完成的计算过程。与普通函数直接返回结果不同的是异步函数返回一个将会包含结果的 Future。该 Future 会在结果准备好时通知调用者。
- Stream 是一系列异步事件的序列。其类似于一个异步的
Iterable
,不同的是当你向Iterable
获取下一个事件时它会立即给你,但是 Stream 则不会立即给你而是在它准备好时告诉你。
创建 Stream
下面是一个周期性发送整数的函数例子:
Stream<int> timedCounter(Duration interval, [int? maxCount]) async* {
int i = 0;
while (true) {
await Future.delayed(interval);
yield i++;
if (i == maxCount) break;
}
}
该函数返回一个 Stream
。而函数体会在该 Stream
被监听时开始运行且以一定的周期间隔在指定的数字范围内不断地生成一个递增数字。如果省略掉 count 参数,那么循环将无休止地执行下去,此时除非取消订阅,否则 Stream
会不停地生成越来越多的数字。
你可以像下面这样使用由 timedCounter()
函数返回的 Stream:
var counterStream = timedCounter(const Duration(seconds: 1), 15);
counterStream.listen(print); // Print an integer every second, 15 times.
当监听器取消时(调用由 listen()
方法返回的 StreamSubscription
对象中的 cancel()
方法),如果下一次循环体执行到 yield
语句,此时该语句的作用类似于 return
语句。而且任意 finally
语句块在此时执行均会导致函数退出。如果函数尝试在退出前 yield
一个值,那么该尝试将会以失败告终并产生类似于 return
语句的效果。
当函数最终退出时,由 cancel()
方法返回的 Future
完成。如果函数是因为出错导致退出,则 Future
完成时会携带对应的错误,否则其会携带一个 null
。
另外,一个更有用的示例是将一个 Future 序列转换为 Stream 的函数:
Stream<T> streamFromFutures<T>(Iterable<Future<T>> futures) async* {
for (final future in futures) {
var result = await future;
yield result;
}
}
该函数循环向 Future 序列请求一个 Future 并等待该 Future 完成获取其结果后提交给 Stream。如果某个 Future 因出错完成,则该错误也会提交给 Stream。
在实际应用中,通过 async* 函数从零构建 Stream 的情况比较少见。 async* 函数通常会根据某些数据源来创建 Stream,而这些数据源常常又是另一个 Stream。比如像上述示例中的 Future 序列,其数据往往来自于其它的异步事件源。然而,在许多情况下,异步函数过于简单难以轻松地处理多个数据源的场景。而这就是 StreamController 类的用武之地。
另一种将Future转换为Stream的方法是通过Stream.fromFutures()
方法:
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
使用 StreamController
如果你 Stream 的事件不仅来自于异步函数可以遍历的 Stream 和 Future,还来自于你程序的不同部分,这种情况使用上述两种方式生成 Stream 就显得比较困难。面对这种情况,我们可以使用一个 StreamController 来创建和填充 Stream。
StreamController 可以为你生成一个 Stream,并提供在任何时候、任何地方将事件添加到该 Stream 的方法。该 Stream 具有处理监听器和暂停所需的所有逻辑。控制器对象你可以自行处理而只需返回调用者所需的 Stream 即可。
下面代码所实现的 timedCounter() 版本(出自 stream_controller.dart)通过使用 StreamController 中的 onListen、onPause、onResume 和 onCancel 回调实现暂停功能。
Stream<int> timedCounter(Duration interval, [int? maxCount]) {
late StreamController<int> controller;
Timer? timer;
int counter = 0;
void tick(_) {
counter++;
controller.add(counter); // Ask stream to send counter values as event.
if (counter == maxCount) {
timer?.cancel();
controller.close(); // Ask stream to shut down and tell listeners.
}
}
void startTimer() {
timer = Timer.periodic(interval, tick);
}
void stopTimer() {
timer?.cancel();
timer = null;
}
controller = StreamController<int>(
onListen: startTimer,
onPause: stopTimer,
onResume: startTimer,
onCancel: stopTimer);
return controller.stream;
}
我们将使用 Stream 的代码更改为如下:
void listenWithPause() {
var counterStream = timedCounter(const Duration(seconds: 1), 15);
late StreamSubscription<int> subscription;
subscription = counterStream.listen((int counter) {
print(counter); // Print an integer every second.
if (counter == 5) {
// After 5 ticks, pause for five seconds, then resume.
subscription.pause(Future.delayed(const Duration(seconds: 5)));
}
});
}
在 listenWithPause() 函数中使用上面的这个 timedCounter 函数,运行后你就可以看到当订阅暂停时打印输出的计数也会暂停,尔后又可以正确地恢复。
你必须使用全部的回调 onListen、onCancel、onPause 和 onResume 来通知暂停状态的变化,否则如果订阅状态与暂停状态在同一时间都改变了,只会有 onListen 或 onCancel 回调会被调用。
使用StreamController需要非常小心,很容易出现问题,更多关于StreamController注意事项参考: 在 Dart 里使用 Stream
接收 Stream 事件
像使用 for 循环 迭代一个 Iterable 一样,我们可以使用 异步 for 循环 (通常我们直接称之为 await for
)来迭代 Stream 中的事件。例如:
该代码只是简单地接收整型事件流中的每一个事件并将它们相加,然后返回(被 Future
包裹)相加后的整型值。当循环体结束时,函数会暂停直到下一个事件到达或 Stream 完成。
内部使用 await for
循环的函数需要使用 async
关键字标记。
下面的示例中使用了 async* 函数生成一个简单的整型 Stream 来测试上一个代码片段:
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
await for (final value in stream) {
sum += value;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
yield i;
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // 55
}
错误事件
当 Stream 再也没有需要处理的事件时会变为完成状态,与此同时,调用者可以像接收到新事件回调那样接收 Stream 完成的事件回调。当使用 await for 循环读取事件时,循环会在 Stream 完成时停止。
有时在 Stream 完成前会出现错误;比如从远程服务器获取文件时出现网络请求失败,或者创建事件时出现 bug,尽管错误总是会有可能存在,但它出现时应该告知使用者。
Stream 可以像提供数据事件那样提供错误事件。大多数 Stream 会在第一次错误出现后停止,但其也可以提供多次错误并可以在在出现错误后继续提供数据事件。在本篇文档中我们只讨论 Stream 最多出现并提供一次错误事件的情况。
当使用 await for 读取 Stream 时,如果出现错误,则由循环语句抛出,同时循环结束。你可以使用 try-catch
语句捕获错误。下面的示例会在循环迭代到参数值等于 4 时抛出一个错误:
Future<int> sumStream(Stream<int> stream) async {
var sum = 0;
try {
await for (final value in stream) {
sum += value;
}
} catch (e) {
return -1;
}
return sum;
}
Stream<int> countStream(int to) async* {
for (int i = 1; i <= to; i++) {
if (i == 4) {
throw Exception('Intentional exception');
} else {
yield i;
}
}
}
void main() async {
var stream = countStream(10);
var sum = await sumStream(stream);
print(sum); // -1
}
Stream 的使用
Stream 类中包含了许多像 Iterable
类中一样的辅助方法帮助你实现一些常用的操作。例如,你可以使用 Stream API 中的 lastWhere()
方法从 Stream 中找出最后一个正整数。
Future<int> lastPositive(Stream<int> stream) =>
stream.lastWhere((x) => x >= 0);
Stream 的两种类型
Single-Subscription 类型的 Stream
最常见的类型是一个 Stream 只包含了某个众多事件序列的一个。而这些事件需要按顺序提供并且不能丢失。当你读取一个文件或接收一个网页请求时就需要使用这种类型的 Stream。
这种 Stream 只能设置一次监听。重复设置则会丢失原来的事件,而导致你所监听到的剩余其它事件毫无意义。当你开始监听时,数据将以块的形式提供和获取。
Broadcast 类型的 Stream
另一种流是针对单个消息的,这种流可以一次处理一个消息。例如可以将其用于浏览器的鼠标事件。
你可以在任何时候监听这种 Stream,且在此之后你可以获取到任何触发的事件。这种流可以在同一时间设置多个不同的监听器同时监听,同时你也可以在取消上一个订阅后再次对其发起监听。
处理 Stream 的方法
下面这些 Stream<T>
类中的方法可以对 Stream 进行处理并返回结果:
Future<T> get first;
Future<bool> get isEmpty;
Future<T> get last;
Future<int> get length;
Future<T> get single;
Future<bool> any(bool Function(T element) test);
Future<bool> contains(Object? needle);
Future<E> drain<E>([E? futureValue]);
Future<T> elementAt(int index);
Future<bool> every(bool Function(T element) test);
Future<T> firstWhere(bool Function(T element) test, {T Function()? orElse});
Future<S> fold<S>(S initialValue, S Function(S previous, T element) combine);
Future forEach(void Function(T element) action);
Future<String> join([String separator = '']);
Future<T> lastWhere(bool Function(T element) test, {T Function()? orElse});
Future pipe(StreamConsumer<T> streamConsumer);
Future<T> reduce(T Function(T previous, T element) combine);
Future<T> singleWhere(bool Function(T element) test, {T Function()? orElse});
Future<List<T>> toList();
Future<Set<T>> toSet();
上述所有的方法,除了 drain()
and pipe()
方法外,都在 Iterable
类中有对应的相似方法。如果你在异步函数中使用了 await for
循环(或者只是在另一个方法中使用),那么使用上述的这些方法将会更加容易。例如,一些代码实现大概是这样的:
Future<bool> contains(Object? needle) async {
await for (final event in this) {
if (event == needle) return true;
}
return false;
}
Future forEach(void Function(T element) action) async {
await for (final event in this) {
action(event);
}
}
Future<List<T>> toList() async {
final result = <T>[];
await forEach(result.add);
return result;
}
Future<String> join([String separator = '']) async =>
(await toList()).join(separator);
转换现有的 Stream
下面的方法可以对原始的 Stream 进行处理并返回新的 Stream。当调用了这些方法后,设置在原始 Stream 上的监听器会先监听被转换后的新 Stream,待新的 Stream 处理完成后才会转而回去监听原始的 Stream。
Stream<R> cast<R>();
Stream<S> expand<S>(Iterable<S> Function(T element) convert);
Stream<S> map<S>(S Function(T event) convert);
Stream<T> skip(int count);
Stream<T> skipWhile(bool Function(T element) test);
Stream<T> take(int count);
Stream<T> takeWhile(bool Function(T element) test);
Stream<T> where(bool Function(T event) test);
在 Iterable 类中也有一些将一个 iterable 转换为另一个 iterable 的方法,上述的这些方法与 Iterable 类中的这些方法相似。如果你在异步函数中使用了 await for 循环,那么使用上述的这些方法将会更加容易。
你可以使用 Stream 类提供的转换类方法,比如 map()、where()、expand()
和 take()
来应对大多数常见的转换需求。
例如,假设你有一个名为 counterStream
的 Stream,用于每秒打印输出一个自增的整数。其实现过程可能如下:
var counterStream =
Stream<int>.periodic(const Duration(seconds: 1), (x) => x).take(15);
你可以使用下面的代码快速查看事件:
counterStream.forEach(print); // Print an integer every second, 15 times.
你可以在监听 Stream 前调用一个类似 map()
的转换方法来转换 Stream 的事件。该方法将返回一个新的 Stream。
// Double the integer in each event.
var doubleCounterStream = counterStream.map((int x) => x * 2);
doubleCounterStream.forEach(print);
你可以使用任意其它的变换方法替代 map()
,比如类似下面的这些:
.where((int x) => x.isEven) // Retain only even integer events.
.expand((var x) => [x, x]) // Duplicate each event.
.take(5) // Stop after the first five events.
通常而言,使用各种转换方法足以满足你简单的使用需求。但是,如果你需要对转换进行更多的控制,你可以使用 Stream 类的 transform()
方法指定一个 StreamTransformer
。 Dart 平台库为许多常见的任务需求提供了 Stream 转换器。例如下面的代码使用了由 dart:convert
库提供的 utf8.decoder
和 LineSplitter
转换器。
Stream<List<int>> content = File('someFile.txt').openRead();
List<String> lines = await content
.transform(utf8.decoder)
.transform(const LineSplitter())
.toList();
下面的代码示例读取一个文件并在其 Stream 上执行了两次变换。第一次转换是将文件数据转换成 UTF-8 编码格式,然后将转换后的数据变换成一个 LineSplitter 执行。文件中除了 # 开头的行外其它的行都会被打印出来。
import 'dart:convert';
import 'dart:io';
void main(List<String> args) async {
var file = File(args[0]);
var lines = utf8.decoder
.bind(file.openRead())
.transform(const LineSplitter());
await for (final line in lines) {
if (!line.startsWith('#')) print(line);
}
}
listen() 方法
这是一个“底层”方法,其它所有的 Stream 方法都根据 listen()
方法定义。
StreamSubscription<T> listen(void Function(T event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError});
你只需继承 Stream 类并实现 listen() 方法来创建一个 Stream 类型的子类。 Stream 类中所有其它的方法都依赖于对 listen()
方法的调用。
listen() 方法可以让你对一个 Stream 进行监听。在你对一个 Stream 进行监听前,它只不过是个惰性对象,该对象描述了你想查看的事件。当你对其进行监听后,其会返回一个 StreamSubscription 对象,该对象用以表示一个生产事件的活跃的 Stream。这与 Iterable 对象的实现方式类似,不同的是 Iterable 对象可返回迭代器并可以进行真实的迭代操作。
Stream 允许你暂停、继续甚至完全取消一个订阅。你也可以为其设置一个回调,该回调会在每一个数据事件、错误事件以及 Stream 自身关闭时通知调用者。
Microtask 微任务
Dart中是一个单线程模型 ,会不断从 事件队列 和 微任务队列 两个队列中取任务执行。
//执行一个微任务
scheduleMicrotask(() {
print('a microtask');
});
Dart VM 模型
每个isolate
区之间是隔离的,每个isolate
区域都由一个 Mutator Thread + N 个 Helper Thread (处理GC)组成
Dart 事件循环机制
类似Android中的Handler
的MessageQueue
消息循环,Dart也是靠event loop
来驱动的。event loop
会不断的从消息队列中获取isolate发来的消息事件来处理,与Android中一个线程只有一个Looper+一个消息队列不同的是,在Dart中有两个队列:一个叫事件队列(event queue),一个叫微任务队列(microtask queue)。后者的优先级较高。
- 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。
- 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。
Dart在执行完main()
方法后,就会由Event-Loop执行两个任务队列中的事件,在每一次事件循环中,总是先去检查微任务队列中是否有可执行的任务,如果有就依次执行Event,然后才会查询事件队列的Event执行,而在执行事件队列的Event时,每执行完一个Event,就会再检查一次微任务队列。 所以微任务队列优先级较高,可以用来插队。
因为 microtask queue 的优先级高于 event queue ,所以如果 microtask queue有太多的微任务, 那么就可能会霸占住当前的event loop。从而对event queue中的触摸、绘制等外部事件造成阻塞卡顿。