Dart中的空安全:防踩坑的利器
写Dart代码的时候,经常会遇到一个烦人的问题:空值(null)。如果一个变量不小心变成了null,程序运行时就会报错,抛出那个让人头疼的“空指针异常”。这在开发圈子里可是个老大难问题。
举个例子:
void main() {
String name; // 没给name赋值
print(name.length); // 编译直接报错
}
上面这段代码,name
没赋值,试图访问它的.length
属性,编译器直接给你一巴掌。为了在代码运行前就抓住这种问题,Dart从2.12版本开始引入了**空安全(Null Safety)**功能。
空安全为啥这么重要?
- 代码更稳:写代码时就能发现运行时的空值问题,防患于未然。
- 类型更清晰:声明一个
String
类型,意思就是这个变量绝对不会是null。 - 性能更优:编译器知道变量肯定有值,就能做更多优化。
来看个简单的例子:
void main() {
String name = "Turabi"; // 非空变量,不能是null
// name = null; // 直接报错,编译器不让
String? surname = null; // 可空变量,可以是null
surname = "Sancak"; // 现在赋了个值
}
解释一下:
String name
→ 非空变量,必须一直有值。String? surname
→ 可空变量,可能有值,也可能是null。
可空和非空变量
空安全的核心就是两个概念:非空变量和可空变量。
- 非空变量:值绝对不能是null,Dart默认所有变量都是这种。
- 可空变量:值可以是null,类型后面得加个
?
。
非空变量例子:
void main() {
String name = "Turabi";
print(name.toUpperCase()); // 没问题,输出:TURABI
// name = null; // 报错,非空变量不能赋null
}
name
必须得有值,编译器压根不让你赋null。
可空变量例子:
void main() {
String? surname = null; // 可以是null
print(surname); // 输出:null
surname = "Sane";
print(surname); // 输出:Sane
}
这里用String?
,说明surname
可以是字符串,也可以是null。
现实中空安全用在哪儿?
- 可选信息:比如用户不一定填姓氏,用
String? surname
。 - 外部数据:API返回的字段可能为空,比如
int? age
。 - 延迟赋值:变量先是null,后续再赋值。
空值检查的几种方法
如果变量是可空的(String?
),操作时得小心,不然可能引发运行时错误。Dart提供了几种检查空值的方法,帮你写出更安全的代码:
1. ?.
— 空安全访问(稳妥操作)
如果变量不是null,就执行右边的操作;如果是null,直接返回null。
void main() {
String? name = "Leroy";
print(name?.toUpperCase()); // 输出:LEROY
name = null;
print(name?.toUpperCase()); // 输出:null
}
为啥用它?
- 变量可能为null,但你只想在它有值时执行操作。
- 遇到null不会报错,直接返回null,稳得一批。
优点:
- 安全,程序不会崩。
- 代码简洁,
name?.toUpperCase()
一目了然。
缺点:
- 如果是null,只返回null,没法提供备选值。
- 如果你不想看到null,得用其他方法。
2. ??
— 空值合并(默认值)
如果变量是null,就用??
后面的值。
void main() {
String? message;
print(message ?? "没消息"); // 输出:没消息
message = "你好";
print(message ?? "没消息"); // 输出:你好
}
这例子展示了Dart空安全怎么通过??
操作符明确处理空值,写出安全的代码。
为啥用它?
- 想在变量为null时给个默认值。
- 特别适合用户没输入值时,给个“兜底”值。
优点:
- null情况完全在你掌控中。
- 代码读起来简单。
缺点:
- 得每次都指定默认值。
- 有时候默认值可能不太合适。
3. ??=
— 空值赋值(仅空时赋值)
只有当变量是null时,才会给它赋值。
void main() {
String? city;
city ??= "利物浦";
print(city); // 输出:利物浦
city ??= "伊斯坦布尔";
print(city); // 还是输出:利物浦(因为已经不是null,不会变)
}
意思是:“只有它本来是null,才给它赋个初始值。”
为啥用它?
- 想在变量为null时赋个值,之后就不改了。
- 特别适合初始化场景。
优点:
- 一行代码搞定检查和赋值。
- 默认值赋值场景写起来超短。
缺点:
- 用多了可能让代码可读性变差。
- 适合初始赋值,但不适合后续更新。
4. !
— 空断言(强制使用)
你跟编译器说:“我打包票,这变量肯定不是null!”如果猜错了,运行时会报错。
void main() {
String? name = "Teoman";
print(name!.toUpperCase()); // 输出:TEOMAN
name = null;
print(name!.toUpperCase()); // 报错:用了个null值
}
重要提醒:这是最后手段!先试试其他安全方法。
为啥用它?
- 编译器觉得变量可能为null,但你确信它不是。
优点:
- 不用写多余的
if
或??
,直接用。
缺点:
- 猜错了就崩,程序直接挂。
- 得100%确定才敢用。
5. 用if
检查空值
最经典的方法就是用if
判断。如果变量不是null,Dart的编译器会在if
块里把变量当非空处理,这叫流分析。
void main() {
String? name;
if (name != null) {
// 在这块儿,name被当做非空的String
print(name.toUpperCase());
} else {
print("name是null");
}
}
编译器看到if (name != null)
,就不需要用!
强制断言。
为啥用它?
- 最传统、最易读的方法。
- 复杂场景(要执行多个操作)时,
if
更清晰。
优点:
- 编译器通过流分析自动把变量当非空处理。
- 代码逻辑一目了然。
缺点:
- 简单场景下,可能会觉得代码有点长。
late
关键字的用法
在Dart里,声明非空变量时,编译器会要求你立刻赋值。但有时候,值得晚点才能确定,这时候就得用late
关键字。
1. late
是啥?
late
意思是:“我现在不给这变量赋值,但保证用之前一定赋上值。”- 你跟编译器打包票:“放心,我会搞定的。”
2. 不用late
会咋样?
void main() {
String token; // 非空变量
print(token); // 报错:非空变量得先赋值才能用
}
没赋值就用,编译器直接报错。
3. 用late
解决问题
void main() {
late String token; // 现在是空的,晚点赋值
token = "gs1905"; // 赋了个值
print(token); // 输出:gs1905
}
用了late
,你告诉Dart:“我稍后会赋值”,编译器就放过你了。
4. late
的坑
如果你没兑现承诺,忘了给late
变量赋值:
void main() {
late String token;
print(token); // 运行时错误:LateInitializationError
}
这时候程序会直接崩。
5. 啥时候用late
?
适合的场景:
- 异步操作:比如从API拿数据后再赋值。
- Flutter控件生命周期:在
initState
里给控制器赋值。 - 依赖注入:保证晚点会提供值的情况。
不适合的场景:
- 值真的可能是可选的(用
String?
更好)。 - 有可能一直是null的情况。
6. late
vs String?
// late
late String name; // 意思是:这变量不会是null,但我晚点赋值。
// nullable
String? surname; // 意思是:这变量可以是null,也可以有值。
区别:
late
:你保证它不会是null。?
:你允许它可能是null。
总结
空安全是Dart的一大杀器,能大大降低现代应用的错误率。它把变量是否可能为null直接融入类型系统,让你在编译和运行时都更安全。
搞清楚String
和String?
的区别,合理用?.
、??
、??=
、!
和if
这些工具,能让你的代码更健壮、更易读。尤其在大项目里,空安全能帮你防住很多意外崩溃。
记住,空安全其实就是在回答这个问题:
“这个变量是一直有值,还是可能为null?”
只要你问对了这个问题,也答对了,空安全就不会是啥复杂问题,反而会是你写出安全、干净代码的得力助手。