准备迎接更安全的 Dart - sound null safety
对于客户端开发而言,一定对 null safety
有所了解。大量现代计算机语言(Swift、Kotlin、TypeScript 等)都引入了强大的 null safety
功能,它的出现能够帮助程序员避免大量使用空对象而产生的 Bug,而且还能够额外改进程序在运行中的性能。
截止到 2020 年 6 月,null safety
进入到了技术预览版中,并计划于年底发布正式版。目前 null safety
还不建议运用到生产环境中,但我们现在就可以提前一探究竟,为年底到来的正式版做一个充足的准备。
理论基础
先来看一下没有 null safety
的世界是什么样的
String name;
print($name); //💥
int square(int value) {
return value * value;
}
square(null); //💥
当尝试访问的值为 null 时,会抛出运行时错误。要避免运行时错误,我们需要在几乎所有地方都加入判断,因为每个对象都有可能为 null 。我们只有在运行时才能够判断出对象到底是不是 null 。
int square(int value) {
assert(value != null);
if (value == null) throw Exception();
return value * value;
}
而 null safety
带来了三个明显的好处:
- 编译期间就能确定对象是否可能为 null ,可以简化判断逻辑,增强代码安全性。
- API 表达的意图会更加明确,例如明确参数和返回值是否可能为 null 。
- 编译器在编译时能够根据类的 nullable 信息来优化我们的代码,提高程序运行效率。
语法
下面的例子都是在 Sound null safety
的环境中运行的,可以在 Dartpad 中进行尝试。
默认类型都不为空
int age;
age = 36;
age = null; //💥
使用 ?
声明 Nullable 类型
int? age;
age = 36;
age = null; // 👌
?
的其他使用场景
// 方法参数
void openSocket(int? port) {
// port can be null
}
// 方法返回值
String? lastName(String fullName) {
final components = fullName.split(' ');
return components.length > 1 ? components.last : null;
}
// 范型
T? firstNonNull<T>(List<T?> items) {
return items.firstWhere((item) => item != null);
}
// 级联运算符新增 ?..
Path? path;
path
?..moveTo(0, 0)
..lineTo(0, 2)
..lineTo(2, 2)
..lineTo(2, 0)
..lineTo(0, 0);
// 直接获取可空数组的元素
int? first(List<int>? items) {
return items?[0];
}
在有把握的时候,使用 !
int? maybeValue = 42;
int value = maybeValue!; // 强制将 nullable 的 maybeValue 转换为 non-nullable
!
对 null 强制解包会抛出运行时错误
String? name;
print(name!); //💥
条件语句优化
int sign(int x) {
int result; // 定义一个 non-nullable 的变量
if (x >= 0) {
result = 1;
}
// 💥
// 编译器根据上下文,判断当前的条件语句所覆盖的情况,
// 会导致部分条件下 result 没有初始化值,因此会在编译期报错。
print(result.abs());
return result;
}
int absoluteValue(int? value) {
if (value == null) {
return 0;
}
// 由于 value 为 null 的时候都会走到上面的条件语句中,
// 因此编译器根据上下文,自动将下面的 value 转换成了 non-nullable
// 这个特性非常符合直觉,在 kotlin 上也能看到
return value.abs();
}
函数的命名参数和位置参数
// dart 方法的命名参数都是可选参数,但在 null safety 环境下,
// value 默认是不为空的,这就造成了悖论。
void printAbs({int value}) { // 💥
print(value.abs());
}
printAbs() // 💥
// 我们可以用三种方法来解决
// 如果参数不允许为空,使用新增的 required 修饰符(替换旧的@required)
void printAbs({required int value}) {
print(value.abs());
}
printAbs(); // The named parameter 'value' is required, but there's no corresponding argument
// 如果参数允许为空,则将参数标记为 nullable
void printAbs({int? value}) {
print(value?.abs());
}
// 如果参数有默认值,可以给一个默认值来保证参数不会为空
void printAbs({int value = 1}) {
print(value.abs());
}
late
关键字
// 两个特点
// 1. late 标注的对象只会在第一次用到它的时候才进行初始化
late final textEditingController = TextEditingController();
// 2. 允许 final 对象延迟初始化
// late 常常与 final 结合使用,能够保证对象的不可变性的同时,推迟初始化的时间。
late final int x; // 虽然使用 final ,但 x 依然可以在声明的时候不初始化。
x = 5;
// 同样适用于全局变量和静态变量
static late int global;
为什么要提一嘴 late
关键字?因为我们在日常 coding 的时候,经常会出现声明的对象是一个不会变的值,但值不能立刻定下来的情况。通常在这种情况下,我们会妥协转而使用 var
,在声明时给值一个 null ,并且在 null safety
环境下还要将类型设置为 nullable 。这个妥协会让我们的代码不安全,也不符合我们的预设。我们可能会在之后的代码编写中修改了这个原版设计不可变的值,这往往很危险。
// 为什么要定义成 nullable?因为没有初始化默认就是 null
String? name;
void main() {
name = 'zac';
// nullable 的另一个问题是,需要额外逻辑来处理 null
print('name is ${name ?? 'zac'}');
name = 'boby';
}
现在,有了 late
关键字,我们可以一开始就将 name 定义为不可变类型,同时类型为 non-nullable
。
late final String name;
void main() {
name = 'zac';
print('name is $name');
}
注意
一开始开发者往往会陷入 nullable
陷阱,其表现就是,项目中充斥着大量的 nullable
不确定类型。很多人抱怨引入 null safety
后项目逻辑变复杂了也是这样原因。为什么会这样?
我们在 null safety
环境下编写代码时,心中需要时刻明确,每一个值的可为空性。对于 non-nullable
的值,一定要从源头将其定义为不可为空类型。但是,我们在实际编码过程中,有些特殊情况会让我们不得不产生出原本不为空但实际定义成可为空的值,例如上面延迟初始化的反例。
这就需要我们正确使用 dart 提供的语法和规范,设计出符合我们预期的代码,并且从源头上避免各种 int?
String?
满天飞的情况。
结语
刚接触 null safety
的人往往会产生抵触心理,毕竟需要面对全新的语法,而且 null safety
会时刻逼迫你在设计每个 API 、定义每个属性的时候都考虑值的可空性。虽然这看起来像是额外的工作,但仔细想想,这些确保代码安全性的工作不是本来就应该需要做的吗?你的项目之所以还没有出现问题,仅仅是因为还没有足够多的边界条件触发那些不安全代码的 Bug。墨菲定律告诉我们,凡是可能出错的事就一定会出错。 null safety
能让你的代码变得更加健壮,减少之后的维护和 Debug 时间。从这一点来看,引入 null safety
绝对是一个划算的买卖。
参考:
转载自:https://juejin.cn/post/6875956749078265864