前言
在现代编程中,空值(null
) 处理一直是开发者面临的挑战之一。空引用可能导致程序崩溃
或产生难以调试的错误
,尤其是在大型项目中。
时至今日,空安全已经是一个屡见不鲜的话题,目前像主流的编程语言Kotlin
、Swift
、Rust
等都对空安全有自己的支持。
Dart
作为一种现代化的编程语言,自版本 2.12
开始引入了 空安全(Null Safety
) 特性,从根本上解决了这一问题
。
空安全不仅提高了代码的健壮性
和可预测性
,还使得开发者能够更加自信地构建和维护复杂的应用程序。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
一、空值问题
空值(null
) 表示一个未初始化
或不存在的对象引用
。虽然空值有时是必要的,但不当使用会导致程序崩溃
或产生难以调试的错误
。例如,在 Java
或 C#
中,访问一个 null
引用的属性或方法会导致 NullPointerException
或类似的异常
。
String name = null;
int length = name.length(); // 这里会抛出 NullPointerException
空指针异常搞得很多人怀疑人生
:
二、什么是空安全?
空安全(Null Safety
) 是一种编程语言特性,旨在防止因变量
或对象引用
为 null
而导致的错误。在支持空安全的语言中,类型系统能够区分 非空类型(Non-nullable Types
) 和 可空类型(Nullable Types
),从而在编译期就捕捉到潜在的空指针异常
,减少运行时错误的发生
。
空安全会在编译期
防止意外访问 null
变量的错误的产生。
健全的空安全通过对非空变量的未被初始化
或以 null
初始化的情况进行标记
,把潜在的 运行时错误 转变成了 编辑时 的分析错误。
简而言之,空安全是通过类型系统重构和编译时静态验证为主、运行时兜底为辅的
核心机制
,将运行时NPE
转化为编译时可预防的错误
。
此特性在开发应用的过程中修复这类错误:
- 没有以
非空的值
初始化 - 赋了
null
值。
示例代码:
// 声明非空变量
var i = 42; // Inferred to be an int.
//声明可空变量
int? aNullableInt = null;
有了空安全后的状态:
三、空安全的原则
空安全支持基于以下两条核心原则:
- 1、默认不可空。除非
将变量显式声明为可空
,否则它一定是非空的类型
。经研究后发现,非空是目前的API
中最常见的选择,所以选择了非空作为默认值
。 - 2、完全可靠。
Dart
的空安全是非常可靠的
,意味着编译期间包含了很多优化
。如果类型系统推断出某个变量不为空
,那么它 永远 不为空。
做个遵守原则的人:
四、空安全的核心理念
通过编译期检查
来确保代码不会意外地尝试使用 null
值。具体来说:
- 非空类型:默认情况下,所有类型都是
非空的
,意味着它们不能为null
。例如,String
表示一个非空字符串
。 - 可空类型:如果一个变量可以为
null
,则需要显式声明为可空类型
,如String?
。
通过这种区分,编译器可以在编译阶段检测到可能的空值问题
,并要求开发者明确处理这些情况,从而避免运行时错误。
五、空安全的优势
- 减少运行时错误:通过
编译期检查
,提前捕获潜在的空指针异常,减少运行时错误的发生
。 - 提高代码质量:明确的空值处理规则使得代码逻辑更加清晰,减少了不必要的
if (x != null)
检查。 - 增强团队协作:空安全特性有助于团队成员更好地理解代码意图,确保每个人都
遵循一致的空值处理规则
。 - 优化性能:避免不必要的
空值检查
和防御性编程
,可以使代码更加简洁高效。
六、引入空安全前后类型系统
的变化
- 1、在引入空安全前
Dart
的类型系统是这样的:这意味着在之前,所有的类型都可以为
Null
,也就是Null类型
被看作是所有类型的子类
。 - 2、在引入空安全之后:
可以看出,最大的变化是将
Null类型
独立出来了,这意味着Null
不再是其它类型的子类型,所以对于一个非Null类型
的变量传递一个Null
值时会报类型转换错误
。
七、空安全操作符
7.1、空合并操作符 (??
)
当左侧表达式
为 null
时,使用右侧表达式的值作为默认值
。
void main() {
String? nullableName;
// 如果 nullableName 为 null,则使用 "Guest" 作为默认值
String displayName = nullableName ?? "Guest";
print(displayName); // 输出: Guest
}
7.2、空感知赋值操作符 (??=
)
只有在左侧表达式
为 null
时,才执行赋值操作。
void main() {
String? nullableName;
// 如果 nullableName 为 null,则赋值为 "Guest"
nullableName ??= "Guest";
print(nullableName); // 输出: Guest
// 如果已有值,则不再赋值
nullableName ??= "Alice";
print(nullableName); // 输出: Guest
}
7.3、安全调用操作符 (?.
)
允许安全地访问对象的属性或调用方法
,如果对象为null
,则返回 null
。
void main() {
String? nullableName;
// 如果 nullableName 为 null,则 length 也为 null
int? length = nullableName?.length;
print(length); // 输出: null
}
7.4、断言操作符 (!
)
用于断言某个表达式不为空
。使用时需谨慎,确保表达式确实不会为 null
,否则会导致运行时异常
。
void main() {
String? nullableName;
// 如果 nullableName 为 null,则会抛出异常
String nonNullName = nullableName!;
// 更安全的做法是先检查是否为 null
if (nullableName != null) {
String nonNullName = nullableName!;
}
// ! 另外一个常见的用处:
bool isEmptyList(Object object) {
if (object is! List) return false;
return object.isEmpty;
}
}
八、延迟初始化late
的使用
对于无法在定义时进行初始化,并且又想避免使用?.
,那么延迟初始化可以帮到你。通过late
修饰的变量,可以让开发者选择初始化的时机,并且在使用这个变量时可以不用?.
。
void main() {
late List urls;//延时初始化
setUrls(List urls) {
this.urls=urls;
}
int getUrlLen() {
return urls.length;
}
延时初始化虽然能为我们编码带来一定便利,但如果使用不当会带来空异常的问题,所以在使用的时候一定保证赋值和访问的顺序,切莫颠倒。
九、required
的作用
required
关键词主要用于函数参数
和构造函数参数
中,以确保调用者必须提供这些参数的值。这是空安全(null safety
)特性的一部分,旨在提高代码的健壮性
和可预测性
。
9.1、构造函数中的required
当定义一个类时,使用 required
可以确保每个实例都必须初始化某些字段。这对于那些对对象状态至关重要的属性非常有用,可以防止忘记设置必要的参数
。
class Person {
final String name;
final int age;
// 使用 required 确保 name 和 age 必须被传递
Person({required this.name, required this.age});
}
void main() {
// 正确的调用方式
var person = Person(name: "Alice", age: 30);
print(person.name); // 输出: Alice
print(person.age); // 输出: 30
// 错误的调用方式(缺少 required 参数)
// var person2 = Person(); // 编译错误:缺少 name 和 age 参数
}
9.2、函数参数中的required
除了构造函数,required
也可以用于普通函数的命名参数
,确保调用者必须提供这些参数。
void greet({required String name, String greeting = "Hello"}) {
print('$greeting, $name!');
}
void main() {
// 正确的调用方式
greet(name: "Alice"); // 输出: Hello, Alice!
greet(name: "Bob", greeting: "Hi"); // 输出: Hi, Bob!
// 错误的调用方式(缺少 required 参数)
// greet(); // 编译错误:缺少 name 参数
}
9.3、required
的作用与优势
- 强制性:确保关键参数不会被遗漏,提高了代码的可靠性。
- 清晰性:明确哪些参数是必需的,使得
API
更加直观易懂。 - 空安全:结合
Dart
的空安全机制,帮助避免潜在的null
值问题。
9.4、注意事项
required
只能用于命名参数
,不能用于位置参数
。位置参数本身就要求按顺序提供。required
参数必须在调用时显式提供
,即使它们有默认值。
十、总结
空安全是一种重要的编程语言特性
,通过区分非空类型
和可空类型
,并结合专用的操作符
,它能够在编译期捕捉潜在的空指针异常
,减少运行时错误的发生
。掌握空安全不仅提升了代码的质量
和健壮性
,还帮助开发者编写更加简洁高效
的代码。
码字不易,记得 关注 + 点赞 + 收藏 + 评论