Dart之空安全

一、前言
什么是空安全呢?
你可以把它理解成代码运行前的一次“提前体检”。
以前很多空值问题要等程序运行到某一行才爆出来,比如一个对象明明是 null,我们却去访问它的方法或属性,程序就可能崩溃。
Dart 从 2.12 开始引入空安全,就是希望把这类潜在问题提前到编译期发现。也就是说,不要等程序跑起来才发现空指针,而是在写代码时就提醒你:这里可能为空,你要处理一下。
这一篇我们系统讲清楚 Dart 空安全:为什么需要它、它改变了什么、常见操作符怎么用、late 和 required 又分别解决什么问题。
二、一句话理解
Dart 空安全就是通过类型系统区分“可为空”和“不可为空”,把可能的空引用错误尽量提前到编译期发现。
简单说:
- 默认类型不可为空。
- 想允许为空,需要加
?。 - 访问可能为空的对象,要用安全写法。
- 非空断言
!要谨慎使用。 late是延迟初始化,不是免死金牌。required用来要求调用方必须传入参数。
三、空值问题
空值 null 本身不是坏东西。
有些场景确实需要表示“暂时没有值”,比如用户还没填写昵称、接口还没返回数据、搜索结果为空。
真正的问题是:一个值可能是 null,但代码却把它当成一定有值来使用。
void main() {
String? name;
// print(name.length); // 错误:name 可能为 null
}
如果没有检查就访问属性或方法,就可能出现空引用问题。
空值常见来源有:
- 声明变量后没有初始化。
- 接口返回字段为空。
- 查找数据失败。
- 开发者主动赋值为
null。 - 某个对象还没有创建完成。
空安全的目标不是消灭 null,而是让 null 的使用变得明确、可检查、可控制。
四、空安全是什么
空安全是一种语言层面的保护机制。
它让 Dart 类型系统明确区分:
- 不可空类型:一定不能是
null。 - 可空类型:允许是
null。
void main() {
String name = 'Dart';
String? nickName;
print(name);
print(nickName);
}
这里:
String表示不可空字符串。String?表示可以为空的字符串。
如果你给不可空变量赋值 null,编译期就会报错。
void main() {
// String name = null; // 错误
String? nickName = null; // 正确
}
这就是空安全最重要的变化:让“可能为空”这件事写在类型上。
五、空安全的原则
5.1 默认不可空
Dart 空安全引入后,默认情况下类型不可为空。
void main() {
int age = 18;
String name = 'Tom';
}
age 和 name 都不能直接变成 null。
如果你确实需要允许为空,就要显式加 ?。
void main() {
int? age;
String? name;
print(age); // null
print(name); // null
}
这个设计让代码意图更清楚:看到 String,就知道它不应该为空;看到 String?,就知道使用前要考虑空值。
5.2 安全可靠
Dart 的空安全不是简单提示,而是类型系统的一部分。
如果 Dart 分析出一个变量不为空,你就可以放心使用它。
void printName(String? name) {
if (name != null) {
print(name.length);
}
}
在 if (name != null) 之后,Dart 知道 name 在这个代码块里不是 null,所以可以访问 length。
这叫类型提升。它让很多代码不需要反复写空值检查。
六、空安全带来的类型系统变化
在没有空安全之前,很多类型都可能隐含地接收 null。这会让空值风险藏在代码里。
引入空安全后:
int表示非空整数。int?表示可能为空的整数。String表示非空字符串。String?表示可能为空的字符串。
void main() {
int count = 1;
int? optionalCount;
print(count);
print(optionalCount);
}
这让类型本身就带有空值信息。
可以这样记:
| 写法 | 含义 |
|---|---|
String |
一定有字符串 |
String? |
可能有字符串,也可能是 null |
int |
一定有整数 |
int? |
可能有整数,也可能是 null |
空安全让“有没有值”不再靠猜,而是直接写在类型里。
七、空安全的优势
7.1 减少运行时错误
空安全把很多空引用风险提前到编译期。
这就像出门前检查钥匙,而不是到门口才发现没带。
7.2 提高代码质量
类型上明确是否可空后,代码意图更清楚。
别人读到 String? name,马上知道这里要考虑空值;读到 String name,就知道它正常情况下不应该为空。
7.3 减少防御性代码
没有空安全时,开发者可能到处写空判断。
有了空安全,很多地方可以交给类型系统提前检查,代码会更干净。
7.4 增强团队协作
团队成员遵循同一套空安全规则后,参数是否必传、字段是否可空、返回值是否可能为空都会更明确。
八、空安全操作符
Dart 提供了一组操作符帮助我们处理可空值。
8.1 空合并运算符 ??
?? 用于在左侧为 null 时提供默认值。
void main() {
int? count;
int result = count ?? 9;
print(result); // 9
}
可以读成:如果 count 有值,就用 count;否则用 9。
它常用于默认文案、默认数字、默认配置。
void main() {
String? nickName;
print(nickName ?? '未设置昵称');
}
8.2 空赋值运算符 ??=
??= 表示:只有变量当前为 null 时,才给它赋值。
void main() {
int? count;
count ??= 9;
print(count); // 9
}
如果变量已经有值,就不会覆盖。
void main() {
int? count = 3;
count ??= 9;
print(count); // 3
}
注意区别:
??返回一个默认值,不一定修改原变量。??=会在变量为空时修改原变量。
8.3 安全调用操作符 ?.
?. 用于安全访问可能为空的对象。
class User {
User(this.name);
final String name;
}
void main() {
User? user;
print(user?.name); // null
}
如果 user 是 null,user?.name 不会继续访问属性,而是返回 null。
再看方法调用:
class GetData {
int getNum(int i) {
return i + 1;
}
}
void feelNull(GetData? data) {
var result = data?.getNum(1);
print(result);
}
void main() {
feelNull(GetData()); // 2
feelNull(null); // null
}
?. 的作用是:对象为空时停下来,不继续访问。
8.4 非空断言操作符 !
! 用于告诉 Dart:我确定这里不是 null。
void main() {
String? name = 'Dart';
print(name!.length);
}
但是它有风险。
void main() {
String? name;
// print(name!.length); // 运行时报错
}
! 不是把空值变成非空值,它只是告诉编译器“相信我”。如果你判断错了,运行时仍然会出错。
所以建议:
- 能用
if (value != null)就先判断。 - 能用
?.和??就安全处理。 - 只有非常确定不为空时,再使用
!。
九、延迟初始化 late
late 用于延迟初始化非空变量。
有些变量逻辑上不应该为空,但声明时还拿不到值。
void main() {
late String token;
token = 'abc123';
print(token);
}
如果使用前没有赋值,会运行时报错。
void main() {
late String token;
// print(token); // 运行时报错
}
late 适合:
- 初始化依赖后续流程。
- Flutter 生命周期中稍后赋值。
- 某些字段必须非空,但无法在构造时立即得到。
但要记住:late 只是延迟初始化,不是跳过初始化。使用前必须赋值。
十、required 关键字
required 用于要求命名参数必须传入。
它常用于函数参数和构造函数参数。
10.1 函数参数中的 required
void hello({required int count}) {
String name = 'Dart';
print('hello, $name! $count');
}
void main() {
hello(count: 520);
}
如果调用时不传 count,编译期就会提示错误。
这能避免“明明这个参数很重要,但调用方忘了传”的问题。
10.2 构造函数参数中的 required
class Car {
Car({required this.name, this.color = 'yellow'});
final String name;
final String color;
void introduce() {
print('$name is $color');
}
}
void main() {
Car truck = Car(name: 'truck');
truck.introduce();
}
这里 name 必须传,color 有默认值,可以不传。
required 的价值是把“必须提供”这件事写进函数签名和构造函数签名里。
十一、常见误区
11.1 以为加了 ? 就不用处理空值
String? name 只是表示它可能为空,不代表可以随便访问。
void printName(String? name) {
// print(name.length); // 错误
}
你仍然需要判断、使用 ?.,或者提供默认值。
11.2 滥用 !
! 写起来快,但风险也高。
String? name;
// print(name!.length); // 运行时风险
优先选择更安全的写法:
print(name?.length ?? 0);
11.3 把 late 当成万能延迟
late 变量如果使用前没有初始化,会直接报错。
它适合“确定会在使用前赋值”的场景,不适合拿来逃避空安全检查。
11.4 忘记 required 的意义
required 不是让参数不可为空,它只是要求调用方必须传这个参数。
void test({required String? name}) {
print(name);
}
void main() {
test(name: null); // 可以传 null,因为类型是 String?
}
是否可空看类型,是否必传看 required。
十二、总结
这一篇我们系统讲了 Dart 空安全。
空安全的核心是:把“可能为空”这件事写进类型系统里,让潜在空引用问题尽量在编译期暴露。
需要记住的关键点:
- 默认类型不可为空。
- 可空类型要加
?。 ??用于提供默认值。??=用于空时赋值。?.用于安全访问属性或方法。!是非空断言,要谨慎。late是延迟初始化,使用前必须赋值。required表示命名参数必须传入。
空安全不是为了让代码写起来麻烦,而是为了让代码更可靠。它把很多“运行时才炸”的问题提前到写代码时暴露出来,让我们更早发现、更早修正。
浙公网安备 33010602011771号