空安全是Dart 2.12版本新增的一项特性,可以有效地避免空指针异常的出现。事实上,空安全特性并不是Dart独有的,Kotlin、Swift、C#、TypeScript等语言都有此特性。在Dart语音中,空安全支持三条核心原则:
• 默认不可空:除非将变量显式声明为可空,否则它默认一定是要是非空的类型。
• 渐进迁移:开发者可以自由地选择迁移的时机,以及需要迁移的代码。并且,在一个项目中可能会同时存在空安全和非空安全的代码。
• 安全可靠:Dart的空安全在编译期间做了很多的性能优化。
由于Dar空安全是2.12 才提供的新功能,所以要在项目中使用空安全,需要在pubspec.yaml中添加版本配置,如下所示。
environment:
sdk: '>=2.12.0 <3.0.0'
当你选择使用 null satety 特性时,所有的类型默认是非空的。例如如果声明了一个 String类型的变量,那么就意味着它一直包含字符串值。如果你想要一个 String 对象能够接收字符串值或null,那么就需要在类型声明后面加上?标识,一个声明为String?类型的变量可以包含字符串值或 null。
String? str1;
String str2;
// OK
str1 = null;
// 报错
str2 = null;
// OK
List strList1 = ['a', null, 'c'];
// 报错
List strList2 = ['a', null, 'c'];
如果确定一个对象或表达式返回值有值,那么就可以使用空断言操作符!强制转为不为空对象,然后可以使用它赋值给非空对象,或访问其属性或方法,如 valiable!.xx。这种情况下,如果对于nullable 对象不加!,编译器就会报错。但是,如果对象本身是null,加!操作符会导致异常。
int? couldReturnNullButDoesnt() => -3;
void main() {
int? nullableInt = 1;
List intListHasNull = [2, null, 4];
int a = nullableInt!;
int b = intListHasNull.first!;
int c = couldReturnNullButDoesnt()!.abs();
print('a is $a.');
print('b is $b.');
print('c is $c.');
}
为了保证空安全特性,Dart 的流分析(flow analysis)已经考虑了空特性。如果一个 nullable 对象不可能有空值,那么就会被当作非空对象处理,例如:
String? str;
if (str != null) {
print(str); //已经确保不为空,不会编译出错
}
有些时候变量、类成员属性或其他全局变量应该是非空的,但是没法在声明的时候直接赋值,这个时候就需要在变量声明的时候加上 late 关键字。当在变量声明的时候加上late关键字后,就是告诉 Dart 如下的内容:
class Meal {
late String _decription; //错误声明
set description(String desc) {
_description = 'Meal Description: $desc';
}
String get description => _description;
}
void main() {
final myMeal = Meal();
myMeal.description = 'Feijoada';
print(myMeal.description);
}
late关键字对处理循环引用还十分有帮助,譬如我们有一个球队和一个教练,球队和教练就存在相互应用的情况。如果没有 late 关键字,我们就只能声明为 nullable,那样到时候使用到时候就很别扭了——需要到处加空断言操作符或者使用if来判断是否为空。
class Team {
late final Coach coach;
}
class Coach {
late final Team team;
}
void main() {
final myTeam = Team();
final myCoach = Coach();
myTeam.coach = myCoach;
myCoach.team = myTeam;
print('搞定!');
}
升级修改时,需要根据调用的方法参数、返回值或声明的属性做如下处理:
environment:
sdk: ">=2.12.0 <3.0.0"
升级完之后,Dio 请求报错DioError [DioErrorType.other]: type ‘Null’ is not a subtype of type ‘Object’。上网搜了,在 issue 里有提到过,但是说是已经解决了。然后按照 issue 里的方法试也不行,后面想了一下,先直接请求百度网页看看是不是 Dio 的问题,结果请求百度网页正常,那就说明是代码自身的问题。最后再定位发现 是我们的 CookieManager 拦截器的请求 headers 设置 Cookie 字段的时候,当_cookie为 null 的时候导致出现空异常了。这时候我们要检查一下,如果_cookie不为空才设置 Cookie。
// CookieManager 之前的代码,_cookie 可能为 null 导致 Dio 报异常
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
options.headers['Cookie'] = _cookie;
return super.onRequest(options, handler);
}
// 修改后
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
// null safety后需要不为空才可以设置
if (_cookie != null) {
options.headers['Cookie'] = _cookie;
}
return super.onRequest(options, handler);
}
从编码的角度来说,null safety特性实际上增加了编码的工作量。但是null safety更像是一个强制的约定,要求接口或类明确参数或属性的是否为空,从而可以简化协作,提高代码的健壮性。
当然,对于第三方库来说就需要特别小心,有些第三方库使用的是 dynamic 声明的场合,目前 Dart 对 dynamic 声明的变量、属性是不做空校验的,这会导致这样声明的出现空异常,例如上面说到的 RequestOptions options的 headers,就是一个 Map