dart学习第 11 节: 空安全(下)—— 安全操作符详解

今天我们将深入学习空安全中的各种安全操作符和实用技巧,掌握这些内容能让我们在处理空值时更加灵活高效,同时保证代码的安全性。

一、空检查运算符(?.):优雅处理可空对象

在空安全中,当我们需要访问可空类型的属性或方法时,直接访问会导致编译错误。而空检查运算符 ?. 可以帮我们简洁地处理这种场景:它会先判断对象是否为 null,如果是则返回 null,否则正常访问属性或方法。

1. 基本用法

dart 复制代码
void main() {
  String? name = "Dart";
  int? length = name?.length; // 使用 ?. 访问 length 属性
  print(length); // 输出:4(name 不为 null,正常返回长度)

  name = null;
  length = name?.length; // name 为 null,返回 null
  print(length); // 输出:null
}

对比传统的 if 判断写法,?. 让代码更简洁:

dart 复制代码
// 传统写法
int? getLength(String? str) {
  if (str != null) {
    return str.length;
  } else {
    return null;
  }
}

// 使用 ?. 的简化写法
int? getLength(String? str) => str?.length;

2. 链式调用

?. 支持链式调用,非常适合处理多层嵌套的可空对象。但要注意:如果链式调用中某个环节为 null,后续赋值操作不会执行

dart 复制代码
class Address {
  String? city; // 城市(可空)
}

class User {
  Address? address; // 地址(可空,需先初始化)
}

void main() {
  User? user = User();
  // 错误示例:user 的 address 未初始化(为 null),赋值操作无效
  user?.address?.city = "Beijing";
  String? city1 = user?.address?.city;
  print(city1); // 输出:null(因 address 为 null,赋值失败)

  // 正确示例:初始化所有环节后再赋值
  user = User();
  user.address = Address(); // 先初始化 address
  user?.address?.city = "Beijing"; // 此时链式调用各环节均非 null
  String? city2 = user?.address?.city;
  print(city2); // 输出:Beijing(赋值成功)

  // 当 user 为 null 时,整个链式调用返回 null
  user = null;
  String? city3 = user?.address?.city;
  print(city3); // 输出:null
}

如果不使用 ?.,链式调用需要多层 if 判断,代码会非常繁琐。


二、强制非空运算符(!):何时必须使用?

! 运算符的核心作用是:当编译器无法通过代码逻辑确认可空变量一定非空 时,由开发者显式担保变量非空性。但在所有分支都被覆盖 的场景中,编译器会自动推断非空,此时无需使用 !

1. 必须使用 ! 的场景:存在未覆盖的分支

dart 复制代码
void main() {
  String? value;

  // 条件分支未完全覆盖(仅处理了偶数情况)
  if (DateTime.now().second % 2 == 0) {
    value = "Even second";
  }

  // 编译器检测到存在 value 未被赋值的分支,必须使用 !
  print(value!.length); // 输出:字符串长度(若进入赋值分支)或崩溃(若未进入)
}

2. 无需使用 ! 的场景:所有分支均赋值

dart 复制代码
void main() {
  String? value;

  // 条件分支完全覆盖(if 和 else 都赋值)
  if (DateTime.now().second % 2 == 0) {
    value = "Even second";
  } else {
    value = "Odd second";
  }

  // 编译器确认所有分支都赋值,无需使用 !
  print(value.length); // 正常输出:字符串长度
}

3. 风险警示

! 本质是 "开发者向编译器的担保",若担保失效(变量为 null),会直接崩溃:

dart 复制代码
void main() {
  String? name = null;
  print(name!.length); // 运行时崩溃:Null check operator used on a null value
}

最佳实践 :能用 if (var != null) 做非空判断的场景,坚决不使用 !


三、late 关键字:延迟初始化非可空变量

在某些场景下,我们无法在声明时初始化非可空变量(如依赖外部数据加载),但能保证在使用前完成初始化。这时可以使用 late 关键字,它允许非可空变量延迟初始化。

1. 解决 "非空变量必须初始化" 问题

dart 复制代码
class User {
  // 非可空变量,无法在声明时初始化
  late String name; // 使用 late 延迟初始化

  // 在构造函数后初始化
  User(String userName) {
    name = userName; // 延迟初始化
  }
}

void main() {
  User user = User("Alice");
  print(user.name); // 输出:Alice
}

2. 延迟加载的特性

late 变量在首次访问时才会执行初始化逻辑,适合资源密集型对象的初始化:

dart 复制代码
// 模拟耗时初始化
String fetchData() {
  print("Fetching data...");
  return "Remote data";
}

void main() {
  late String data = fetchData(); // 声明时不执行 fetchData()
  print("Before accessing data");
  print(data); // 首次访问时执行 fetchData()
  print(data); // 再次访问时直接使用缓存值
}

// 输出:
// Before accessing data
// Fetching data...
// Remote data
// Remote data

3. 风险:未初始化就访问会崩溃

late 变量如果在初始化前被访问,会抛出运行时异常,因此必须确保使用前完成初始化:

dart 复制代码
void main() {
  late String value;
  // print(value); // 运行时崩溃:LateInitializationError: Field 'value' has not been initialized
  value = "Hello"; // 初始化后才能使用
}

四、空值合并运算符(??)与空值赋值运算符(??=)

除了上述操作符,Dart 还提供了两个实用的空值处理运算符:

1. 空值合并运算符(??):提供默认值

?? 用于在变量为 null 时返回默认值,否则返回变量本身:

dart 复制代码
void main() {
  String? name = null;
  String displayName = name ?? "Guest"; // name 为 null,使用默认值
  print(displayName); // 输出:Guest

  name = "Bob";
  displayName = name ?? "Guest"; // name 非 null,使用 name 的值
  print(displayName); // 输出:Bob
}

2. 空值赋值运算符(??=):仅在为 null 时赋值

??= 用于仅当变量为 null 时才赋值,避免覆盖已有值:

dart 复制代码
void main() {
  String? message = null;
  message ??= "Hello"; // message 为 null,赋值
  print(message); // 输出:Hello

  message ??= "World"; // message 非 null,不赋值
  print(message); // 输出:Hello
}

这两个运算符常与 ?. 结合使用,处理复杂的空值场景:

dart 复制代码
class User {
  String? name;
}

String? getUserName(User? user) {
  return user?.name ?? "Unknown"; // 若 user 或 name 为 null,返回默认值
}

void main() {
  print(getUserName(null)); // 输出:Unknown(user 为 null)
  print(getUserName(User())); // 输出:Unknown(name 为 null)
  print(getUserName(User()..name = "Charlie")); // 输出:Charlie
}

五、空安全最佳实践

掌握空安全的关键不仅是语法,更重要的是形成良好的编程习惯:

  1. 优先使用非可空类型:默认选择非可空类型,仅在必要时使用可空类型。
  2. 谨慎使用 ! 运算符:仅在编译器确实无法推断非空性,且开发者能 100% 确保变量非空时使用。
  3. 合理使用 latelate 适合依赖外部条件的延迟初始化,避免为了绕过编译错误而滥用。
  4. 利用类型提升 :通过 if (var != null) 让编译器自动推断非空性,减少显式操作符:
dart 复制代码
void printLength(String? text) {
  if (text != null) {
    // 编译器自动提升 text 为非可空类型
    print(text.length); // 无需使用 !
  }
}
  1. 初始化列表中初始化非可空变量:在构造函数中通过初始化列表确保非可空变量被初始化:
dart 复制代码
class Person {
  String name;
  int age;

  // 初始化列表中赋值
  Person({required String n, required int a}) : name = n, age = a;
}
相关推荐
叽哥2 小时前
dart学习第 13 节:异步编程基础 —— Future 与 async/await
flutter·dart
xiaoyan20153 小时前
基于flutter3.32+window_manager仿macOS/Wins风格桌面os系统
前端·flutter·dart
weixin_4111918413 小时前
原生安卓与flutter混编的实现
android·flutter
会煮咖啡的猫1 天前
编写 Flutter 游戏摇杆组件
flutter
来来走走1 天前
Flutter dart运算符
android·前端·flutter
风清云淡_A1 天前
【Flutter3.8x】flutter从入门到实战基础教程(五):Material Icons图标的使用
前端·flutter
阳光明媚sunny1 天前
Flutter基础知识
flutter
新镜1 天前
【Flutter】双路视频播放方案
flutter·音视频
GeniuswongAir1 天前
flutter分享到支付宝
flutter