前言
在现代编程中,空值(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参数必须在调用时显式提供,即使它们有默认值。
十、总结
空安全是一种重要的编程语言特性,通过区分非空类型和可空类型,并结合专用的操作符,它能够在编译期捕捉潜在的空指针异常,减少运行时错误的发生。掌握空安全不仅提升了代码的质量和健壮性,还帮助开发者编写更加简洁高效的代码。
码字不易,记得 关注 + 点赞 + 收藏 + 评论