変数宣言
キーワード変数
変数はvarを介して宣言されます。宣言時に変数に値が割り当てられている場合、型はすでに決定されており、再度割り当てるときに他の型を変更することはできません。
var str = "hello dart";
// 因为声明时已经赋值为String,所以不能更改类型
str = 3; // 编译器会报错
str = "nihao"; //再次赋值只能为String类型
Dart自体は強い型の言語であるため、どの変数にも明確な型があります。Dartでは、変数がvarで宣言されると、Dartはコンパイル時に、最初に割り当てられたデータの型に従ってその型を推測し、コンパイルが終了します。次に、そのタイプが決定されました。
動的キーワードとオブジェクト
Objectは、Dartのすべてのオブジェクトの基本クラスです。つまり、DartのすべてのタイプはObjectのサブクラス(FunctionとNullを含む)であるため、Objectによって宣言されたオブジェクトに任意のタイプのデータを割り当てることができます。dynamicおよびObjectによって宣言された変数は、任意のオブジェクトに割り当てることができ、割り当てのタイプは、varとは異なり、次のように後で変更できます。
dynamic str = "hi dart!";
str = 4;
Object src = 34.32;
src = "nihao";
このように割り当てる場合は問題ありませんが、現時点では、動的とオブジェクトの両方を使用して任意のタイプに変数を割り当てることができるため、この2つの違いは何ですか?dynamicとObjectの違いは、動的に宣言されたオブジェクトのコンパイラがすべての可能な組み合わせを提供するのに対し、 Objectによって宣言されたオブジェクトはObjectのプロパティとメソッドのみを使用できることです。そうでない場合、コンパイラは次のようなエラーを報告します。
dynamic a = "hi dart!";
Object b = "nihao";
// 正常打印 值为8
print(a.length);
// 报错
print(b.length);
Objectで変更された変数bはlength属性を使用できません。これは、Objectオブジェクトがこの属性を持たず、dynamicで変更された変数がStringタイプの属性とメソッドを持っているため、length属性を使用できるためです。
但是这里有个点需要注意: dynamic修饰的变量a,使用a.length
没有报错,而且能正常打印值,是因为 a 被赋值为String 类型,而String类型有length这个属性,但是如果我们把length改成其他String类型没有的属性,比如随便写一个不存在的属性:
print(a.ccccc); // a是字符串,没有"ccccc"属性,编译时不会报错,运行时会报错
这样写在编译期不会报错,但运行时会报错,这一点要注意。
关键字 final和const
final 和const 都是修饰一个不可变的变量,一个final变量只能被设置一次,两者区别在于:const 变量是一个编译时常量(编译时直接替换为常量值),final变量在第一次使用时被初始化。被final或者const修饰的变量,变量类型可以省略,如:
//可以省略String这个类型声明
final str = "hi dart";
//final String str = "hi dart";
const str1 = "hi dart";
//const String str1 = "hi dart";
空安全(null-safety)
Dart 中一切都是对象,这意味着如果我们定义一个数字,在初始化它之前如果我们使用了它,假如没有某种检查机制,则不会报错,比如:
test() {
int i;
print(i*8);
}
在 Dart 引入空安全之前,上面代码在执行前不会报错,但会触发一个运行时错误,原因是 i 的值为 null 。但现在有了空安全,则定义变量时我们可以指定变量是可空还是不可空。
由于空安全机制,将运行时的错误提前到编译期,这样在我们对这个变量操作时,提前发现这个变量是否为空,从而避免运行时崩溃。
int i = 8; // 默认不为空,使用前必须初始化
int? j; // 定义为可空类型,对于可空变量,我们在使用前必须判空。
// 如果我们预期变量不能为空,但在定义时不能确定其初始值,则可以加上late关键字,
// 表示会稍后初始化,但是在正式使用它之前必须得保证初始化过了,否则会报错
late int k;
k = 9;
如果一个变量我们定义为可空类型,在某些情况下即使我们给它赋值过了,但是预处理器仍然有可能识别不出,这时我们就要显式(通过在变量后面加一个“!”符号)告诉预处理器它已经不是null了,比如:
class Test {
int? i;
Function? fun;
say() {
if (i != null) {
print(i! * 8); // 因为已经判过空,所以能走到这 i 必不为null,如果没有显式申明,则 IDE 会报错
}
if (fun != null) {
fun!(); // 同上
}
}
}
上面中如果函数变量可空时,调用的时候可以用语法糖:
fun!.call(); // fun不为空时,调用call
List 和 Map
在Dart 中数组等于列表,声明一个集合和map可以使用:
var list = [];
var map = {};
以上声明相当于java中的:List list = new List()
和 Map map = new Map()
上面的声明之后,list中的元素和map中的key-value类型都是dynamic。
因为Dart中没有基本数据类型,所以在list中,可以给元素赋值任意类型,而他们基类都是Object,map同理。
函数
Dart是一种真正的面向对象的语言,所以即使是函数也是对象并且有一个类型Function。这意味着函数可以赋值给变量或者作为参数传递给其他函数,这就是函数式编程的典型特征。
函数声明
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理,注意,函数返回值没有类型推断:
对于只包含一个表达式的函数,可以使用简写语法:
bool isTrue() => true;
函数作为变量
var say = (str) {
print(str); // 打印:hi dart!
};
say("hi dart!");
函数作为参数传递
void execute(var callback) {
callback();
}
execute(() => print("xxx"))
上面的例子,就是把一个() => print("xxx")
函数当作参数传递给callback,然后execute里执行callback()函数,也就是执行了传进去的那个函数,打印:"xxx"。
1. 可选的位置参数
包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:
String say(String from, String msg, [String? device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
下面是一个不带可选参数调用这个函数的例子:
say('Bob', 'Howdy'); //结果是: Bob says Howdy
下面是用第三个参数调用这个函数的例子:
say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal
2. 可选的命名参数
定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。例如:
//设置[bold]和[hidden]标志
void enableFlags({bool bold, bool hidden}) {
// ...
}
调用函数时,可以使用指定命名参数。例如:paramName: value
enableFlags(bold: true, hidden: false);
可选命名参数在Flutter中使用非常多。注意,不能同时使用可选的位置参数和可选的命名参数。
mixin
Dart不支持多继承,但是他支持mixin,简单来讲 mixin 可以 “组合” 多个类,我们通过一个例子来理解。 定义一个 Person 类,实现吃饭、说话、走路和写代码功能,同时定义一个 Dog 类,实现吃饭、和走路功能:
class Person {
say() {
print('say');
}
}
mixin Eat {
eat() {
print('eat');
}
}
mixin Walk {
walk() {
print('walk');
}
}
mixin Code {
code() {
print('key');
}
}
class Dog with Eat, Walk{}
class Man extends Person with Eat, Walk, Code{}
我们定义了几个 mixin,然后通过 with 关键字将它们组合成不同的类。有一点需要注意:如果多个mixin 中有同名方法,with 时,会默认使用最后面的 mixin 的,mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。
异步支持
Dart类库有非常多的返回Future
或者Stream
对象的函数,这些函数被称为异步函数:他们只会在设置好一些耗时操作之后返回,比如像IO操作,而不是等到这个操作完成。
而 async
和await
关键字支持了异步编程,允许您写出和同步代码很像的异步代码。
1. Future
Future
与JavaScript中的Promise
非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future
只会对应一个结果,要么成功,要么失败。 由于本身功能较多,这里我们只介绍其常用的API及特性。还有,请记住,Future
的所有API的返回值仍然是一个Future
对象,所以可以很方便的进行链式调用。
1)Future.then
为了方便示例,在本例中我们使用Future.delayed
创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即2秒后返回结果字符串"hi world!",然后我们在then
中接收异步结果并打印结果,代码如下:
Future.delayed(Duration(seconds: 2), () {
return "hi world";
}).then((value) => print(value));
2)Future.catchError
如果异步任务发生错误,我们可以在catchError中捕获错误,我们将上面示例改为:
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print("success");
}).catchError((e){
//执行失败会走到这里
print(e);
});
在本示例中,我们在异步任务中抛出了一个异常,then
的回调函数将不会被执行,取而代之的是 catchError
回调函数将被调用;但是,并不是只有 catchError
回调才能捕获错误,then
方法还有一个可选参数onError
,我们也可以用它来捕获异常:
Future.delayed(Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
3)Future.whenComplete
有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如:在网络请求前弹出加载对话框,在请求结束后关闭对话框,这种场景,有两种方法:
- 分别在
then
或catch
中关闭一下对话框 - 使用
Future
的whenComplete
回调
我们将上面示例改一下:
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});
4)Future.wait
有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到UI界面上,应该怎么做?答案是Future.wait
,它接受一个Future
数组参数,只有数组中所有Future
都执行成功后,才会触发then
的成功回调,只要有一个Future
执行失败,就会触发错误回调。下面,我们通过模拟Future.delayed
来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:
Future.wait([
// 2秒后返回结果
Future.delayed(Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(Duration(seconds: 4), () {
return "world";
})
]).then((result) {
print(result[0] + result[1]);
}).catchError((e) {
print(e);
});
执行上面代码,4秒后你会在控制台中看到“hello world”。
2. async/await
Dart中的async/await
的意思就是:异步任务串行化,可以避免我们在使用过程中遇到的回调地狱问题。 先来看看什么是回调地狱:
1)回调地狱(Callback Hell)
如果代码中有大量异步逻辑,并且出现大量异步任务依赖其他异步任务的结果时,必然会出现Future.then回调中套回调的情况。举个例子:现在有个需求场景是用户先登录,登录成功后会获得用户ID,然后通过用户ID,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下:
//先分别定义各个异步任务
Future<String>? login(String userName, String pwd){
...
//用户登录
};
Future<String>? getUserInfo(String id){
...
//获取用户信息
};
Future? saveUserInfo(String userInfo){
...
// 保存用户信息
};
接下来,执行整个任务流:
login("admin", "xxx")?.then((id) {
getUserInfo(id)?.then((userInfo) {
saveUserInfo(userInfo)?.then((value) {
// ...
});
});
});
可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback Hell) 。回调地狱问题在之前 JavaScript 中非常突出,也是 JavaScript 被吐槽最多的点,但随着 ECMAScript 标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是 ECMAScript6 引入了Promise
,以及ECMAScript7 中引入的async/await
。 而在 Dart 中几乎是完全平移了 JavaScript 中的这两者:Future
相当于Promise
,而async/await
连名字都没改。接下来我们看看通过Future
和async/await
如何消除上面示例中的嵌套问题。
2)消除回调地狱
消除回调地狱主要有两种方式:
一、使用Future消除Callback Hell
login("alice","******").then((id){
return getUserInfo(id);
}).then((userInfo){
return saveUserInfo(userInfo);
}).then((e){
//执行接下来的操作
}).catchError((e){
//错误处理
print(e);
});
正如上文所述, “Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用” ,如果在then
中返回的是一个Future
的话,该future
会执行,执行结束后会触发后面的then
回调,这样依次向下,就避免了层层嵌套。
二、使用 async/await 消除 callback hell
通过Future
回调中再返回Future
的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用async/await
了,下面我们先直接看代码,然后再解释,代码如下:
task() async {
try {
String? id = await login("admin", "xxx");
String? userInfo = await getUserInfo(id!);
await saveUserInfo(userInfo!);
//执行接下来的操作
} catch(e) {
//错误处理
print(e);
}
}
async
用来表示函数是异步的,定义的函数会返回一个Future
对象,可以使用then
方法添加回调函数。await
后面是一个Future
,表示等待该异步任务完成,异步完成后才会往下走。await
必须出现在async
函数内部。
可以看到,我们通过async/await
将一个异步流用同步的代码表示出来了。
Stream
Stream
也是用于接收异步事件数据,和 Future
不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream
常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:
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: (){
});
上面的代码依次会输出:
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3