dart学习第 10 节: 空安全(上)—— 为什么需要空安全?

155 阅读5分钟

从今天开始,我们将深入探讨 Dart 语言中一个非常重要的特性 ——空安全(Null Safety) 。空安全是 Dart 2.12 引入的重大特性,它从根本上改变了我们处理空值的方式,让代码更健壮、更可靠。本节课我们先理解 “为什么需要空安全” 以及它的基础概念。

一、空指针异常:程序员的 “噩梦”

在开始学习空安全之前,我们先搞清楚一个问题:为什么要费劲引入空安全?答案很简单 —— 为了消灭空指针异常(Null Pointer Exception,简称 NPE)

1. 什么是空指针异常?

当你试图访问一个值为 null 的变量的属性或调用其方法时,就会触发空指针异常。比如:

void main() {
  String name; // 未初始化的变量(在非空安全模式下默认可为 null)
  print(name.length); // 尝试访问 null 的 length 属性
}

在没有空安全的情况下,这段代码会在运行时崩溃,抛出类似 NoSuchMethodError: The getter 'length' was called on null 的错误。

2. 空指针异常的危害(生产环境案例)

空指针异常看似简单,却是生产环境中最常见的崩溃原因之一,可能造成严重后果:

  • 用户体验受损:App 突然闪退,导致用户数据丢失或操作中断。

    • 案例:某购物 App 在结算页面因未判断地址是否为 null,导致用户付款后订单崩溃,引发大量投诉。
  • 经济损失:服务端程序因 NPE 崩溃,可能导致服务中断。

    • 案例:某支付系统在峰值时段因空指针异常宕机 10 分钟,直接损失数百万交易流水。
  • 调试困难:运行时异常往往需要结合具体场景复现,尤其是在复杂系统中,定位 null 出现的位置可能耗费大量时间。

3. 为什么空指针异常如此普遍?

根本原因是:在传统编程模式中,变量是否允许为 null 是模糊的

  • 开发者可能忘记初始化变量
  • 函数返回值可能在某些边缘 case 下为 null
  • 调用者可能误传 null 作为参数

这些问题在代码量庞大或多人协作时尤为突出。



二、Dart 空安全的核心思想:“明确可空性”

Dart 空安全的解决方案可以总结为一句话:让变量的可空性变得明确,由编译器在编译时检查,而不是等到运行时崩溃

简单说就是:

  • 默认情况下,所有变量都是非可空的(不能为 null
  • 如果变量可能为 null,必须显式声明为可空类型

这种设计将空指针异常的检测从 “运行时” 提前到 “编译时”,让错误在开发阶段就能被发现。



三、非可空类型与可空类型

1. 非可空类型(Non-nullable)

在空安全模式下,未加任何修饰的类型都是非可空的,必须被初始化且不能赋值为 null

void main() {
  String name; // 错误:非可空变量必须初始化
  name = null; // 错误:不能将 null 赋值给非可空类型
  print(name);

  // 正确写法:声明时初始化
  String username = "dart";
  int age = 20;
  bool isStudent = true;

  // 正确:后续赋值也不能为 null
  username = "flutter"; // ✅
  // username = null; // ❌ 编译错误
}

非可空类型的变量从诞生到销毁,始终保证有一个有效值,彻底避免了因意外 null 导致的崩溃。

2. 可空类型(Nullable)

如果变量可能为 null,需要在类型后加 ? 修饰符,声明为可空类型。

void main() {
  // 可空类型变量可以初始化为 null 或具体值
  String? name = null; // ✅
  int? age = 20; // ✅

  // 可空类型变量可以后续赋值为 null
  name = "dart"; // ✅
  name = null; // ✅

  age = null; // ✅
}

? 就像一个 “标签”,告诉编译器和其他开发者:“这个变量可能为 null,使用时要小心!”

3. 可空类型的使用限制

正因为可空类型可能为 null,Dart 对其使用施加了限制:不能直接访问可空变量的属性或方法

void main() {
  String? name;

  // 错误:直接访问可空变量的属性
  print(
    name.length,
  ); // ❌ 编译错误:The property 'length' can't be unconditionally accessed because the receiver can be 'null'.

  // 正确:先判断不为 null 再使用(类型提升)
  if (name != null) {
    print(name.length); // ✅ 此时编译器知道 name 不为 null
  }
}

这个限制是关键 —— 它强制开发者处理 null 情况,避免了无意识的空指针调用。



四、编译时检查 vs 运行时检查

空安全的核心价值在于将空指针异常的检查从 “运行时” 提前到 “编译时”,这两者有本质区别:

维度编译时检查(空安全)运行时检查(传统模式)
发生时机代码编写 / 编译阶段程序运行阶段(可能在生产环境)
发现方式编译器直接报错,无法通过编译程序崩溃,抛出异常
影响范围仅开发者可见,不影响用户可能导致服务中断、用户数据丢失等严重后果
修复成本开发阶段即可修复,成本低需复现问题、定位原因,复杂系统中修复成本高
可靠性从源头避免空指针异常,代码更可靠依赖开发者手动判断 null,容易遗漏边缘 case

举例说明

// 编译时检查(空安全模式)
void printLength(String? text) {
  // 编译错误:提示 text 可能为 null
  print(text.length); // ❌ 开发者立即知道需要处理 null
}

// 修复后:
void printLength(String? text) {
  if (text != null) {
    print(text.length); // ✅ 编译通过,运行时绝不会因 text 为 null 崩溃
  } else {
    print("文本为空");
  }
}

在传统非空安全模式下,这段代码能正常编译,但当 text 为 null 时,会在运行时崩溃。而空安全模式下,编译器会强制开发者处理 null 情况,从根本上避免了这类错误。