2.4 空安全(Null Safety)

基础知识

空安全(Null Safety)是 Dart 2.12 版本引入的一项重大特性,旨在帮助开发者在编译时捕获空引用错误,从而减少运行时崩溃。在启用空安全后,Dart 编译器会严格检查变量是否可能为 null,并强制开发者在代码中明确处理 null 的可能性。这使得代码更加健壮和可靠。

1. 可空类型与非空类型

在启用空安全后,Dart 中的类型默认是非空的。这意味着一个变量在没有明确声明为可空的情况下,不能被赋值为 null

  • 非空类型:默认情况下,所有类型都是非空的。例如 Stringintbool
    dart
    复制代码
    String name = "Alice";
    // name = null; // 编译时错误:A value of type 'Null' can't be assigned to a variable of type 'String'.
  • 可空类型:通过在类型后面添加 ? 来声明可空类型。例如 String?int?bool?
    dart
    复制代码
    String? nullableName; // 可以是 String 类型的值,也可以是 null
    nullableName = "Bob";
    nullableName = null; // 合法

2. 空安全操作符

为了安全地处理可空类型,Dart 引入了几个新的操作符:

  • 空检查 (!):断言表达式不为 null。如果表达式为 null,则会抛出运行时错误 Null check operator used on a null value。只有当你非常确定某个可空变量在特定时刻不会为 null 时才使用它。
    dart
    复制代码
    String? name = getUsername(); // 假设 getUsername 可能返回 null
    print(name!.length); // 如果 name 为 null,这里会抛出运行时错误
  • 空值合并 (??):如果左侧表达式为 null,则返回右侧表达式的值;否则返回左侧表达式的值。
    dart
    复制代码
    String? username;
    String displayName = username ?? "Guest"; // 如果 username 为 null,displayName 为 "Guest"
    print(displayName); // 输出: Guest
    
    username = "Alice";
    displayName = username ?? "Guest";
    print(displayName); // 输出: Alice
  • 空值合并赋值 (??=):如果变量为 null,则将右侧表达式的值赋给变量。
    dart
    复制代码
    String? message;
    message ??= "Default message"; // 如果 message 为 null,则赋值 "Default message"
    print(message); // 输出: Default message
    
    message ??= "Another message"; // message 不为 null,不会再次赋值
    print(message); // 输出: Default message
  • 条件成员访问 (?.):如果对象不为 null,则访问其成员;否则返回 null。这可以避免在访问 null 对象的成员时抛出 NoSuchMethodError
    dart
    复制代码
    String? text;
    print(text?.length); // 输出: null (不会抛出错误)
    
    text = "hello";
    print(text?.length); // 输出: 5

3. 类型提升 (Type Promotion)

Dart 编译器足够智能,可以在某些情况下自动将可空类型“提升”为非空类型。例如,在 if 语句中检查一个可空变量是否为 null 后,在该 if 块内部,该变量的类型会被提升为非空类型。

dart
复制代码
void printLength(String? text) {
  if (text != null) {
    // 在这个 if 块内部,text 的类型从 String? 提升为 String
    print(text.length); // 可以安全地访问 length 属性
  }
}

void main() {
  printLength("hello"); // 输出: 5
  printLength(null); // 不会输出,也不会报错
}

4. late 关键字

late 关键字用于延迟初始化非空变量。当你声明一个非空变量,但不能在声明时立即初始化它,并且你确定它在使用前一定会被初始化时,可以使用 late。这在某些场景下非常有用,例如:

  • StatefulWidgetState 类中声明非空变量,并在 initState 方法中初始化。
  • 声明一个非空变量,其初始化依赖于其他变量,而这些变量在声明时还不可用。
dart
复制代码
late String description;

void initDescription() {
  description = "This is a late initialized string.";
}

void main() {
  initDescription();
  print(description); // 输出: This is a late initialized string.

  // 如果在使用前没有初始化,会抛出运行时错误
  // late int value;
  // print(value); // 运行时错误:LateInitializationError
}

官方文档链接

Flutter 开发中的应用案例

空安全是 Flutter 开发中非常重要的一部分,它贯穿于整个框架和我们编写的应用程序代码中。理解并正确使用空安全特性,可以显著减少运行时错误,提高代码质量和开发效率。

案例:一个用户个人资料页面,处理可能为空的数据

我们将创建一个用户个人资料页面,其中包含用户的姓名、邮箱和电话号码。这些信息可能从后端获取,并且某些字段可能为空。我们将演示如何使用空安全特性来安全地处理这些可空数据,并优雅地显示在 UI 上。

dart
复制代码
import 'package:flutter/material.dart';

// 模拟用户数据模型,包含可空字段
class UserProfile {
  final String name;
  final String? email; // 邮箱可能为空
  final String? phoneNumber; // 电话号码可能为空

  UserProfile({
    required this.name,
    this.email,
    this.phoneNumber,
  });
}

class UserProfileScreen extends StatefulWidget {
  const UserProfileScreen({super.key});

  @override
  State<UserProfileScreen> createState() => _UserProfileScreenState();
}

class _UserProfileScreenState extends State<UserProfileScreen> {
  UserProfile? _userProfile; // 用户资料可能为空,因为需要异步加载
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _fetchUserProfile();
  }

  Future<void> _fetchUserProfile() async {
    // 模拟网络请求延迟
    await Future.delayed(const Duration(seconds: 2));

    // 模拟加载成功或失败,以及数据为空的情况
    setState(() {
      // _userProfile = UserProfile(name: '张三', email: 'zhangsan@example.com', phoneNumber: '13800138000');
      // _userProfile = UserProfile(name: '李四', email: null, phoneNumber: '13912345678');
      _userProfile = UserProfile(name: '王五', email: 'wangwu@example.com', phoneNumber: null);
      // _userProfile = null; // 模拟数据加载失败或用户不存在
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('用户个人资料'),
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator()) // 加载中
          : _userProfile == null
              ? const Center(child: Text('用户资料加载失败或不存在。')) // 数据为空
              : Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        '姓名: ${_userProfile!.name}', // 使用 ! 断言 _userProfile 不为 null
                        style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                      ),
                      const SizedBox(height: 16),
                      // 使用 ?. 和 ?? 安全地访问可空字段
                      Text(
                        '邮箱: ${_userProfile!.email ?? '未提供'}', // 如果 email 为 null,显示 '未提供'
                        style: const TextStyle(fontSize: 18),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        '电话: ${_userProfile!.phoneNumber ?? '未提供'}', // 如果 phoneNumber 为 null,显示 '未提供'
                        style: const TextStyle(fontSize: 18),
                      ),
                      const SizedBox(height: 24),
                      // 示例:使用条件成员访问 ?. 访问可空字段的属性
                      if (_userProfile!.email != null)
                        Text(
                          '邮箱长度: ${_userProfile!.email!.length}', // 再次使用 ! 断言 email 不为 null
                          style: const TextStyle(fontSize: 16, color: Colors.grey),
                        ),
                      if (_userProfile!.phoneNumber != null)
                        Text(
                          '电话号码包含数字: ${_userProfile!.phoneNumber!.contains(RegExp(r'[0-9]'))}',
                          style: const TextStyle(fontSize: 16, color: Colors.grey),
                        ),
                    ],
                  ),
                ),
    );
  }
}

void main() {
  runApp(const MaterialApp(home: UserProfileScreen()));
}

案例分析:

  • final String? email;final String? phoneNumber;:在 UserProfile 类中,emailphoneNumber 被声明为可空类型(String?),明确表示它们可能为 null。这在编译时就强制我们处理 null 的可能性。
  • UserProfile? _userProfile;:在 _UserProfileScreenState 中,_userProfile 也被声明为可空类型,因为在数据加载完成之前,它可能为 null
  • _userProfile == null ? ... : ...:在 build 方法中,我们首先检查 _userProfile 是否为 null。如果为 null,则显示“用户资料加载失败或不存在”的提示。这是一种常见的空安全处理模式,确保在访问对象成员之前,对象本身不为 null
  • _userProfile!.name:一旦我们确定 _userProfile 不为 null(例如在 _userProfile == null 检查之后),我们就可以使用空检查操作符 ! 来断言它不为 null,从而安全地访问其非空成员 name注意: 滥用 ! 会导致运行时错误,应谨慎使用,确保在调用前已进行 null 检查。
  • _userProfile!.email ?? '未提供':这里使用了空值合并操作符 ??。如果 _userProfile!.emailnull,则显示字符串 '未提供'。这是一种优雅地为可空字段提供默认值的方式,避免了冗长的 if-else 语句。
  • _userProfile!.email!.length:在访问可空字段的成员时,如果该字段本身也是可空的,则需要再次进行空检查或使用 ! 断言。例如,_userProfile!.email!.length 中,第一个 ! 断言 _userProfile 不为 null,第二个 ! 断言 email 不为 null
  • if (_userProfile!.email != null):在某些情况下,我们可能需要根据可空字段是否为 null 来决定是否显示某个 UI 元素。这里使用 if 语句结合 != null 检查来实现。

这个案例清晰地展示了 Dart 空安全特性在 Flutter 应用中的实际应用。通过合理地使用可空类型、空安全操作符和类型提升,我们可以编写出更安全、更健壮的 Flutter 代码,有效避免空引用异常,提升应用质量。