AIGC标识 Dart之空安全

Dart之空安全

一、前言

什么是空安全呢?

你可以把它理解成代码运行前的一次“提前体检”。

以前很多空值问题要等程序运行到某一行才爆出来,比如一个对象明明是 null,我们却去访问它的方法或属性,程序就可能崩溃。

Dart 从 2.12 开始引入空安全,就是希望把这类潜在问题提前到编译期发现。也就是说,不要等程序跑起来才发现空指针,而是在写代码时就提醒你:这里可能为空,你要处理一下。

这一篇我们系统讲清楚 Dart 空安全:为什么需要它、它改变了什么、常见操作符怎么用、laterequired 又分别解决什么问题。

二、一句话理解

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';
}

agename 都不能直接变成 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
}

如果 usernulluser?.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 表示命名参数必须传入。

空安全不是为了让代码写起来麻烦,而是为了让代码更可靠。它把很多“运行时才炸”的问题提前到写代码时暴露出来,让我们更早发现、更早修正。

posted @ 2026-07-19 15:51  好的佩奇  阅读(20)  评论(0)    收藏  举报