关于Flutter空安全的一些使用经验和理解
Posted 静水流深zz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Flutter空安全的一些使用经验和理解相关的知识,希望对你有一定的参考价值。
前言
Flutter 2.0的使用已经有一段时间了,想在此分享一些关于空安全的使用经验和个人理解。
Dart空安全
空安全是什么?
在空安全下,运行时的NullPointer Exception
错误被提前到了开发阶段。
即:
void main() {
// 在空安全下, 开发阶段就会报错,而非运行时
String name;
debugPrint(name);
}
类型上也有了变化,这里贴两张官方的类型关系图深入理解空安全 :
非空安全时代:
空安全时代
可以看到,Null
这个类型变成了一个单独的类,而非所有类的子类,换言之:
void main() {
String a = null;
}
非空安全下:
因为Null是所有类型的子类,基于多态性的原理,这种书写方式是正确的。
在空安全下:
Null独立了出去,那么当你再像上面那样书写时,就会报错了,因为这本质上发生了类型转换的错误.
空安全有什么?
新增了关键字,并对原有关键字的含义做了拓展 :
? -> 可空, 如: int a?;
! -> 非空 如: int b = a!;
late -> 延迟初始化 如: late int a;
required -> 可选参数的不可空 如: {required int a}
而含义的对象就是开发工具了,下面逐一对它们进行介绍。
关键字: ?
我们在开发过程中,相当多的参数变量并不一定为必传,因此我们可以用 ?
来标注其为可空。
///通过问号,我们可以告知编辑器(及使用者),style是可空的(有点像可选参数)
Widget buildTextWidget(String text, TextStyle? style) {
return Text(text, style: style);
}
调用时:
void main() {
buildTextWidget('hello world !',null);
buildTextWidget('hello world !',customStyle);
}
当然,上面的方法还可以改为使用可选参数:
///命名参数
Widget buildTextWidget(String text, {TextStyle? style}) {
return Text(text, style: style);
}
///位置参数
Widget buildTextWidget(String text, [TextStyle? style]) {
return Text(text, style: style);
}
我个人很喜欢用命名参数,因为在使用时会显示对应参数名,这样使用者可以大致知道他传入的变量用来做什么。
说到这,我们就来介绍下 required
。
关键字: required
当我们希望通过命名参数来提高方法的调用便捷性和可读性时,会遇到一个问题,即:可选参数是可以不传的。
如果参数是可空的,那么还好,如果遇到不可空的就麻烦了:
/// 此时编辑器就会报错,因为 i 变量可能为空
Demon invoke(Target target, {Invoker invoker, String? way, String? material}) {
...
}
当遇到上面的情况时,而我们又确实需要使用这个变量,那么有3种解决办法:
1、 不使用可选参数
2、 设置默认值
3、 使用required 告知编辑器此参数不能为空 (无法使用-方法 2)
修改后:
///此时可以通过编译
Demon invoke(Target target, {required Invoker invoker, String? way, String? material}) {
...
}
/// 调用时,如果不传 参数 invoker, 或者传入空,那么编辑器将会报错
invoke(a); //错误
invoke(a, invoker: null); // 错误
在实际开发中,我们可能需要通过某个方法来初始化一个成员变量,如下类:
class WarLock {
//报错
Demon cardDemon;
Demon invoke(Target target, {required Invoker invoker, String? way, String? material}) {
...
}
}
当我们写完后,会发现变量 cardDemon
报错,编辑器要求你必须对它初始化。我们可以直接加个 ?
告诉编辑器此值可空。
Demon? cardDemon;
那么,当我们随后使用它的时候,编辑器就会要求我们进行判空处理(否则无法通过编译),如果使用位置非常多那就蛋疼了。此外,对于关键变量,这种方法会对后期维护或者接手的小伙伴产生误导。
某些情境下,成员变量需要方法来(如:构造函数内依赖入参)初始化。
同时可以确定,后续的使用总是在其初始化之后。那么此处使用 '?' 将是不严谨的。
因此,late
就应运而生了。
关键字: late
它可以告诉编辑器:这个非空变量,我稍后会初始化。
class WarLock {
late Demon cardDemon;
//如果我们规定此变量不可变,那么我们还可以这样写
// late final Demon cardDemon;
Demon invoke(Target target, {required Invoker invoker, String? way, String? material}) {
...
}
}
此时,我们再使用这个变量时,编辑器将会不会报错,同时也不需要判空。
而这里也就引申出一个问题: 空安全并不意味着不会出现空指针的异常。
class Test{
late int a;
void showMsg() {
debugPrint('$a'); //编辑器并不会报错,类似的问题后面还会出现
}
}
接下来我们看最后一个关键字。
关键字: !
当我们在使用可空的变量时,如果在一个方法块(如:if)内进行了判空,并做了空退出,同方法块内的后续使用依然会出现编辑器的报错 :
class TestEntity{
int? count;
...
void doStuff() {
count = 0;
if(count == null) return;
//报错:A value of type 'int?' can't be assigned to a variable of type 'int'.
int b = count;
}
}
这时,我们就需要使用 !
来告诉编辑器,我确认这个值不会为空。
int b = count!; //不会报错
另一方面,从Flutter的单线程
和Isolate特性
的角度来看,执行到 int b = count;
时是不可能为空的,为什么还会报错呢?
因为 空安全 是语言机制,而非Flutter机制,在方法内使用成员变量时,并不能确保是否有其他线程对其进行了置空操作。(Flutter是支持多Isolate运行的)
其次,不考虑上面的情况,我们依然可以写出 空异常 的代码 :
class TestEntity{
int? count;
...
///调用一个异步方法
void doStuff() async {
...
count = 0;
trigerNullException();
...
}
Future trigerNullException() async {
if(count == null) return ;
//代码会等待1秒后,继续执行
await Future.delayed(const Duration(seconds: 1), () {
count = null;
});
// 此时将会抛出:
// Unhandled Exception: Null check operator used on a null value
int b = count!;
}
}
Tip: 实际上这里的异步调度还是借助了其他线程(Engine层)。
诚然,上方代码Future.delayed()
中的错误一目了然,但在实际开发过程中, 取而代之的可能是一个有着复杂调用链的方法(甚至多人负责的),那么我们就很难把控执行到int b = c!;
时, count 不为空了。
空安全带来了什么?
在我看来,空安全为开发设计规范增加了 “法律效力”。
非空时,我们可以随意定义变量、参数和方法,
class A{
int a;
String txt;
double height;
A(this.a, this.txt,{this.height});
String generateContent({String rawTxt}) {
return '$rawTxt : $height';
}
}
对于使用者,我们只能通过参数类型必选参数 或 可选参数
来判断是否为可空 (前提是双方遵守这个约定俗成的规矩)
,而其核心与否就只能期盼注释的阐明了。
方法的使用,更是只能采取保守策略,对返回结果增加一些安全措施。再复杂一些的,就只能去找对应开发同学了~
空安全下,上述方式的编写风格,将会在开发阶段被标红,设计规范被强制化。
如:入参的要求和个数,设计时需要更为严谨和收敛,因为每一个变量我们都需要处理,否则将无法通过编译。
我们在定义类、参数变量及方法时可以(也需要)通过 空安全关键字 : ? 、 ! 、 required 、late
来告知使用者,哪些变量是核心(不仅限于非空)、而哪些不是;这个方法肯定能返回结果(非空),那个方法可能返回结果 ,
String methodOne() {...}
String? methodTwo() {...}
而 开发工具(空安全) 则为这个规范得以被遵守提供了有力的保障(无法编译)。
这也算是被动提高了开发和使用规范。
一言以蔽之:分锅更为明确~ 哈哈哈
结语
最后,谢谢大家的阅读,如有错误欢迎指出。
其他Flutter相关文章
系列文章
以上是关于关于Flutter空安全的一些使用经验和理解的主要内容,如果未能解决你的问题,请参考以下文章
如何在简单的flutter-dart代码中使用对象snapshot.data实现空安全?