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;
}
相关推荐
孤鸿玉18 小时前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
叽哥1 天前
Flutter Riverpod上手指南
android·flutter·ios
BG2 天前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng2 天前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭2 天前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯2 天前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan2 天前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓2 天前
Flutter Getx 的页面传参
flutter
火柴就是我3 天前
flutter 之真手势冲突处理
android·flutter
Speed1233 天前
`mockito` 的核心“打桩”规则
flutter·dart