一、前言
在dart 2.12
版本之前,任何一种类型都可以为null
,所有的类型默认都是null
,这样也就意味着我们每次使用一个变量的时候,可能都需要判断一个变量是否为空,否则就可能抛出NoSuchMethodError
异常,这样在我们的开发中就很头痛,一不小心就会出现NoSuchMethodError
异常,导致程序崩溃,为了减少这种情况的发生,我们就不得不在代码中添加很多的冗余判断代码。
后来官方可能也是实在容忍不了这种现象,就像其他语言比如Swift
、Kotlin
一样,引入了健全的空安全检查机制。
下面这个例子,就是目前我们在没有引入空安全时,要面临的问题。我们声明一个变量,并且调用它的一些API
,在使用之前必须要判断是否为空:
int nullableInt; //null
if (nullableInt != null) {
print('nullableInt ==== ${nullableInt.toDouble()}');
} else {
print('nullableInt ==== $nullableInt');//打印结果:nullableInt ==== null
}
//使用前没有判空
print('nullableInt ==== ${nullableInt.toDouble()}');
//抛出异常
Unhandled exception:
NoSuchMethodError: The method 'toDouble' was called on null.
Receiver: null
Tried calling: toDouble()
二、什么是空安全?
空安全是一种对可能为空的变量在编译期就进行校验的一种审查机制。这种检查机制对所有变量的默认值也进行了改变,以前默认为null
,现在除非时候显式声明为可为空,所有都按照非空处理,在可能为null
的情况下,如果不进行非空判断,dart
的分析器就会报错,从而减少我们在开发中的NoSuchMethodError
情况的出现。
三、使用
要使用空安全机制,需要把dart版本设置为2.12版本及以上,具体环境配置在pubspec.yaml
文件中,如果想声明一个可空变量,需要显示在类型后面添加?
。
name: dart_study
description: A simple command-line application.
# version: 1.0.0
# homepage: https://www.example.com
environment:
sdk: '>=2.12.0 <3.0.0' #修改dart环境版本,支持空安全
#dependencies:
# path: ^1.7.0
dev_dependencies:
pedantic: ^1.9.0
3.1 基本类型
创建变量时,如果想要一个变量可为null
,需要显示声明为nullable类型,具体就是使用?
进行声明。比如String?
、int?
、double?
等等。看下面的实例:
例子1:显式创建一个可为null
的int
类型变量
int? nullableInt;
print(nullableInt.toDouble()); //The method 'toDouble' can't be unconditionally invoked because the receiver can be 'null'.
这种变量在使用时,必须要判断是否为空,直接使用话,编译不通过,会抛出异常,告知使用了可空的变量
bin/dart_study.dart:4:21: Error: Method 'toDouble' cannot be called on 'int?' because it is potentially null.
Try calling using ?. instead.
print(nullableInt.toDouble());
^^^^^^^^
正确的写法应该是
print(nullableInt?.toDouble() ?? '为空了');
例子2:创建一个非空的int
类型变量,如果判断是否为null
时,会有个警告,这时我们就可以减少不必要的判断,而直接使用变量。
int nonNullInt = 7;
print(nonNullInt);
if (nonNullInt != null) {//The operand can't be null, so the condition is always true.
print(nonNullInt);
}
3.2 集合类型
对于List
、Map
、Set
类型来说,在使用时,不仅要看集合变量是否可空,还要看里面的数据类型是否可空。对于List
和Set
我们可以大致总结为下面的表格:
类型 | 列表是否可为空 | 元素是否可为空 | 说明 |
---|---|---|---|
List<String> | 否 | 否 | 一个包含非空字符串的非空列表 |
List<String>? | 是 | 否 | 一个包含非空字符串的可空列表 |
List<String?> | 否 | 是 | 一个包含可空字符串的非空列表 |
List<String?>? | 是 | 是 | 一个包含可空字符串的可空列表 |
var nullableList = [1, null, 3];
print(nullableList[0]?.toDouble()); //校验空
var nonNullList = [1, 2, 3];
print(nonNullList[0].toDouble());//无需校验空
同样,对于Map
类型也可以总结为一个表格:
类型 | Map是否可为空 | 元素是否可为空 | 说明 |
---|---|---|---|
Map<String: int> | 否 | 否 | 所有元素不可空的非空Map,但取值结果表面不可为空,如果使用的是无效Key,取出的是空 |
Map<String: int>? | 是 | 否 | 所有元素不可空的可空Map,但取值结果表面不可为空,如果使用的是无效Key,取出的是空 |
Map<String: int?> | 否 | 是 | 元素可空的非空Map,key是否有效取值都可为空 |
Map<String: int?>? | 是 | 是 | 元素可空的可空Map,key是否有效取值都可为空 |
Map
和List
、Set
不同的一点是,尽管Map规定了元素非空,但是如果使用的是无效Key取值的话,仍然返回的是一个可空值,所以默认情况下,我们在使用变量接收取出的值时,一般都需要使用可空类型接收。
var nullableMap = <String, int?>{'one': 1};
int? result = nullableMap['two'];
print('取出的值为${result?.toDouble()}'); //必须校验空
var nonNullMap = <String, int>{'one': 1};
print('取出的值为${nonNullMap['two']?.toDouble()}'); //必须校验空
四、空断言运算符
当然,如果我们确定某个Key是有值的,但是编辑器认为它可能为null,这个时候我们不想判断一次,我们可以使用!
进行一次强转,
但是如果不能确定有值的情况,万万不可使用这种方式。
var nullableMap = <String, int?>{'one': 1};
int resultOne = nullableMap['one']!; //我们能够确保’one‘一定有值
print('取出的值为${resultOne.toDouble()}');
五、关键字 late
顾名思义,晚点。。。我们可以使用这个关键字对变量进行修饰,使用late
修饰的变量,可以认为是一种懒加载,分析器也不会要求我们立刻对一个非空类型进行赋值,只要我们能够保证在第一次使用的时候进行赋值,就可以了。
比如我们定义一个类Person
:
class Person {
int age;
String name;
///
/// Non-nullable instance field 'age' must be initialized. (Documentation)
/// Try adding an initializer expression, or add a field initializer in this constructor,
/// or mark it 'late'.
///
Person();
}
我们可以通过添加late
关键字,延迟加载:
class Person {
late int age;
late String name;
Person(); //编译器不会报错
}