Dart 编码规范:正确处理 null
前言
在 Dart 编码中,我们会经常遇到需要处理 null
的场合。Dart 2.12版本引入 null safety
以后,对 null
的处理有了新的规范。关于 null safety 特性,可以阅读本人的另一篇文章:升级踩坑,聊聊 Dart 的 null safety,本篇介绍如何正确处理 null
。
规范1:不要显式地将变量初始化为 null
如果一个变量声明的时候是 non-null
的话,赋值为 null
时,编译器会报错;而如果一个变量声明为 nullable
的话,会隐式地以 null
初始化,因此如果再赋值 null
那就是多此一举了。在 Dart 中不存在未初始化的内存问题,因此没必要初始化为 null
。
// 正确示例
Item? bestDeal(List<Item> cart) {
Item? bestItem;
// ...
}
// 错误示例
Item? bestDeal(List<Item> cart) {
Item? bestItem = null;
// ...
}
复制代码
规范2:不要为函数参数设置 null 默认值
如果设置一个函数的参数是 nullable 的话,同样的,也会隐式地复制 null,因此没必要重复设置默认值。
代码语言:javascript复制// 正确示例
void error([String? message]) {
print(message ?? '未知错误');
}
// 错误示例
void error([String? message = null]) {
print(message ?? '未知错误');
}
复制代码
规范3:使用 ?? 操作符将 null 转换为布尔值
我们有时候会在条件表达式中处理 null
的情况,这个时候更安全的做法是使用 ?? 将为空的对象转换为布尔值。虽然使用 ==
操作符判断也能达到目的,但是并不推荐这么做。对比下面的两个示例就会发现,转换为布尔值的代码具有如下优点:
- 明确表示这段代码有处理
null
值。 - 因为本身就是处理布尔变量,使用
== true
初看看起来会觉得有点多余,似乎可以删掉。 - 使用
?? false
或?? true
可以很清晰地表示如何处理 null。而使用== true
这种代码就有点绕了,读起来会更费脑一些。
// 正确示例
// 将 null 转为 false
if (optionalThing?.isEnabled ?? false) {
print('Have enabled thing.');
}
// 将 null 转为 true
if (optionalThing?.isEnabled ?? true) {
print('Have enabled thing or nothing.');
}
//错误示例
// 将 null 当做 false 处理
if (optionalThing?.isEnabled == true) {
print('Have enabled thing.');
}
// 将 null 当做 true 处理
if (optionalThing?.isEnabled != false) {
print('Have enabled thing or nothing.');
}
复制代码
有一个例外就是如果提前做了 null
判断的话,那么就没必要使用 ??
操作符了,那样反而会增加代码的迷惑性。比如下面这种情况,显然第一方式可读性更高。
// 正确示例
int measureMessage(String? message) {
if (message != null && message.isNotEmpty) {
// message is promoted to String.
return message.length;
}
return 0;
}
// 错误示例
int measureMessage(String? message) {
if (message?.isNotEmpty ?? false) {
// message is not promoted to String.
return message!.length;
}
return 0;
}
复制代码
规范4:如果你代码里会检查变量是否初始化的话,那么就不要使用 late
使用 late
的场景是你明确知道这个变量会在使用前被初始化。如果一个 late
变量没有被初始化被直接使用的话会抛出异常。有时候,我们可能会使用另外一个布尔值来标识 late
变量是否被初始化,但这有点多余。既然可以使用 布尔值跟踪变量是否初始化,那么使用 null
来初始化,再通过检查变量是否是 null
能够达到同样的效果。 只有一种情况,那就是 null 本身也是一个有实际意义的赋值,那么就需要使用布尔值标识是否初始化。比方说我们从后台请求一个不存在的对象,后端可能直接返回 null
,这个 null
就是有意义的。
规范5:将 nullable 成员属性复制为局部变量来提升类型
当我们检测一个变量是否为 null
的时候,对于 nullable
对象如果不为空的话就会提升为 non-null
类型。这样的好处是,当我们再使用这个变量的时候代码会简化很多,比如访问对象属性的时候,无需使用 !
来强制将 nullable
对象转为 non-null
对象。
而对于类成员属性来说,如果直接引用nullable 类成员属性的话,即便做了非空判断也还是需要使用 !
来强制将 nullable
对象转为 non-null
对象。这个时候使用局部变量来复制就可以避免这种问题。通过示例会很明显展示这种好处。
// 正确示例
class NullablePromotionTest {
Offset? offset;
printOffset() {
var offset = this.offset;
if (offset != null) {
print('${offset.dx}, ${offset.dy}');
}
}
}
// 错误示例
class NullablePromotionTest {
Offset? offset;
printOffset() {
if (offset!= null) {
print('${offset!.dx}, ${offset!.dy}');
}
}
}
复制代码
这其实是一个小技巧,但是使用局部变量做 non-null
提升的时候要特别注意,如果可能更新这个成员属性的话,那么需要在局部变量改变后赋值给成员属性。因此,这个一般用于只读场合,涉及变更成员属性的话这种写法稍不注意可能导致 bug。
总结
本篇介绍了 Dart 代码中处理 null
的推荐规范。 不遵循这些规范虽然不会影响代码的正常运行,但是确可能带来导致 bug 的隐患,或影响代码的可读性。因此,在遇到 null
的处理时,可以思考一下如何编写代码能够更好地理解代码逻辑和简化重复的强制性转换。