空安全

11 阅读9分钟

Flutter 空安全规范指南

本文档旨在帮助团队成员理解和正确使用 Dart 空安全特性,避免常见的空指针错误。


第一部分:为什么要处理空安全

1.1 什么是空安全

Dart 2.12 引入了****健全的空安全(Sound Null Safety)** **,这是一种类型系统特性,用于区分可以为 null 的值和不能为 null 的值。


// 非空类型 - 不能赋值为 null

String name = "张三";

  


// 可空类型 - 可以赋值为 null

String? nickname = null;

1.2 为什么需要空安全

❌ 不处理空安全的风险
  1. 运行时崩溃

   ```dart

   // 危险代码

   String? name = null;

   print(name.length);  // 💥 运行时崩溃:Null check operator used on a null value

   ```

  1. 难以调试

   - 空指针错误可能在任何地方发生

   - 错误信息不明确,难以定位问题根源

   - 生产环境中的崩溃影响用户体验

  1. 代码质量下降

   - 大量防御性代码

   - 逻辑混乱,可读性差

✅ 处理空安全的好处
  1. 编译时检查 - 在代码运行前发现潜在的空指针问题

  2. 代码更健壮 - 明确表达哪些值可以为空

  3. 更好的 IDE 支持 - 自动补全和错误提示更准确

  4. 性能优化 - 编译器可以生成更高效的代码


第二部分:核心概念

2.1 可空类型 vs 非空类型


// 非空类型 - 必须有值

String name = "张三";

int age = 18;

List<String> items = [];

  


// 可空类型 - 可以为 null

String? nickname;        // 默认为 null

int? score;              // 默认为 null

List<String>? data;      // 默认为 null

2.2 空值检查操作符

?. 安全调用操作符

String? name = null;

  


// ❌ 危险

print(name.length);  // 崩溃

  


// ✅ 安全

print(name?.length);  // 输出 null

?? 空值合并操作符

String? name = null;

  


// 如果 name 为 null,使用默认值

String displayName = name ?? "未知用户";

??= 空值赋值操作符

String? name;

name ??= "默认名称"// 只有当 name 为 null 时才赋值

! 强制解包操作符(谨慎使用)

String? name = "张三";

  


// 告诉编译器:我确定这个值不为 null

print(name!.length);

  


// ⚠️ 警告:如果 name 为 null,会崩溃!

2.3 late 关键字


class MyWidget extends StatefulWidget {

  // late 表示:这个变量会在使用前被初始化

  late TextEditingController controller;

  

  @override

  void initState() {

    super.initState();

    controller = TextEditingController();  // 在使用前初始化

  }

}

使用场景

  • 变量在声明时无法初始化,但在使用前一定会被初始化

  • 延迟初始化昂贵的对象

⚠️ 注意:如果 late 变量在使用前未初始化,会抛出 LateInitializationError

2.4 dynamic 类型的风险

dynamic 是 Dart 中的一个特殊类型,它会绕过编译时类型检查,导致空安全失效。

为什么 dynamic 危险

// dynamic 类型绕过编译检查

dynamic value = null;

  


// 编译器不会报错,但运行时会崩溃!

print(value.length);  // 💥 NoSuchMethodError: The getter 'length' was called on null

dynamic vs Object? vs var

| 类型 | 编译检查 | 可为 null | 说明 |

|------|----------|---------|------|

| dynamic | ❌ 无 | ✅ 是 | 危险,绕过所有检查 |

| Object? | ✅ 有 | ✅ 是 | 安全,需要类型转换 |

| Object | ✅ 有 | ❌ 否 | 安全,非空 |

| var | ✅ 有 | 取决于初始化 | 类型推断 |

常见的 dynamic 来源
  1. JSON 解析

   ```dart

   // json.decode 返回 dynamic

   dynamic data = json.decode(response.body);

   print(data['name'].length);  // 💥 如果 name 不存在或为 null

   ```

  1. Map 的值类型

   ```dart

   Map<String, dynamic> json = {'name': null};

   String name = json['name'];  // 💥 编译通过,运行崩溃

   ```

  1. 不明确的参数类型

   ```dart

   void process(dynamic data) {  // ❌ 不推荐

     print(data.name);  // 编译器不知道 data 有没有 name

   }

   ```

✅ 正确做法

// 1. JSON 解析时明确类型

Map<String, dynamic> json = jsonDecode(response.body);

String name = json['name'] as String? ?? ''// ✅ 类型转换 + 空值处理

  


// 2. 使用模型类

class User {

  final String name;

  User.fromJson(Map<String, dynamic> json)

      : name = json['name'] as String? ?? '';

}

  


// 3. 使用泛型代替 dynamic

void process<T>(T data) {  // ✅ 泛型更安全

  // ...

}

  


// 4. 使用 Object? 代替 dynamic

void handleAny(Object? data) {  // ✅ 需要类型检查

  if (data is String) {

    print(data.length);  // 编译器知道这里是 String

  }

}

项目中的实际案例

// ❌ 危险:CacheUtils 返回 dynamic

dynamic value = CacheUtils.getValeDynamic(SharedPreferencesUntils.Device_ID);

if (value == null || value == "") {  // ✅ 这里有检查,但依赖运行时

  // ...

}

  


// ✅ 更安全的写法

String? deviceId = CacheUtils.getValeDynamic(SharedPreferencesUntils.Device_ID) as String?;

if (deviceId == null || deviceId.isEmpty) {

  // ...

}


第三部分:项目中如何预防空安全问题

3.1 开启严格的分析器规则

analysis_options.yaml 中配置严格的 lint 规则,让编译器帮你发现问题:


# analysis_options.yaml

analyzer:

  errors:

    # 将这些警告升级为错误

    avoid_dynamic_calls: error

    avoid_returning_null_for_void: error

    null_check_on_nullable_type_parameter: error

  language:

    strict-casts: true      # 禁止隐式类型转换

    strict-raw-types: true  # 禁止原始泛型

  


linter:

  rules:

    # 空安全相关

    - avoid_null_checks_in_equality_operators

    - avoid_returning_null_for_void

    - null_check_on_nullable_type_parameter

    - unnecessary_nullable_for_final_variable_declarations

    

    # dynamic 相关

    - avoid_dynamic_calls

    

    # 类型相关

    - always_declare_return_types

    - always_specify_types  # 可选,较严格

    - avoid_types_on_closure_parameters

    

    # 其他最佳实践

    - prefer_final_locals

    - prefer_const_declarations

3.2 使用代码生成工具

避免手写 JSON 解析代码,使用代码生成工具自动处理空安全:

推荐工具:
  1. json_serializable - 官方推荐

   ```yaml

   # pubspec.yaml

   dependencies:

     json_annotation: ^4.8.1

   dev_dependencies:

     build_runner: ^2.4.6

     json_serializable: ^6.7.1

   ```

  1. freezed - 更强大的不可变模型

   ```yaml

   dependencies:

     freezed_annotation: ^2.4.1

   dev_dependencies:

     freezed: ^2.4.5

   ```

示例:

import 'package:json_annotation/json_annotation.dart';

  


part 'user.g.dart';

  


@JsonSerializable()

class User {

  final String name;           // 必填字段

  final String? avatar;        // 可选字段

  @JsonKey(defaultValue: 0)

  final int age;               // 带默认值

  

  User({required this.name, this.avatar, required this.age});

  

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);

}

3.3 建立空安全工具类

创建统一的工具方法来处理空值:


// lib/utils/null_safety_utils.dart

  


/// 空安全工具类

class NullSafetyUtils {

  /// 安全获取字符串,如果为 null 返回默认值

  static String safeString(dynamic value, [String defaultValue = '']) {

    if (value == null) return defaultValue;

    return value.toString();

  }

  

  /// 安全获取整数

  static int safeInt(dynamic value, [int defaultValue = 0]) {

    if (value == null) return defaultValue;

    if (value is int) return value;

    if (value is String) return int.tryParse(value) ?? defaultValue;

    if (value is double) return value.toInt();

    return defaultValue;

  }

  

  /// 安全获取布尔值

  static bool safeBool(dynamic value, [bool defaultValue = false]) {

    if (value == null) return defaultValue;

    if (value is bool) return value;

    if (value is String) return value.toLowerCase() == 'true';

    if (value is int) return value != 0;

    return defaultValue;

  }

  

  /// 安全获取列表

  static List<T> safeList<T>(dynamic value) {

    if (value == null) return <T>[];

    if (value is List) return value.cast<T>();

    return <T>[];

  }

}

使用示例:

// 从 JSON 中安全获取值

Map<String, dynamic> json = {'name': null, 'age': '25'};

  


String name = NullSafetyUtils.safeString(json['name'], '未知');  // "未知"

int age = NullSafetyUtils.safeInt(json['age']);  // 25

3.4 使用 Extension 扩展方法


// lib/extensions/null_safety_extensions.dart

  


extension NullableStringExtension on String? {

  /// 如果为 null 或空字符串,返回 true

  bool get isNullOrEmpty => this == null || this!.isEmpty;

  

  /// 如果为 null 或空字符串,返回默认值

  String orDefault([String defaultValue = '']) => isNullOrEmpty ? defaultValue : this!;

}

  


extension NullableListExtension<T> on List<T>? {

  /// 如果为 null 或空列表,返回 true

  bool get isNullOrEmpty => this == null || this!.isEmpty;

  

  /// 安全获取元素

  T? safeGet(int index) {

    if (this == null || index < 0 || index >= this!.length) return null;

    return this![index];

  }

}

使用示例:

String? name = null;

print(name.isNullOrEmpty);  // true

print(name.orDefault('未知'));  // "未知"

  


List<String>? items = ['a', 'b'];

print(items.safeGet(0));  // "a"

print(items.safeGet(10));  // null

3.5 建立代码审查流程

  1. PR 提交前

   - 运行 flutter analyze 检查所有警告

   - 确保没有 dynamic 相关警告

  1. Code Review 时

   - 检查本文档第六部分的审查清单

   - 特别关注异步回调中的 setState

  1. 定期扫描

   - 使用 dart fix --apply 自动修复部分问题

   - 定期检查项目中的 ! 使用情况


第四部分:变量声明规范

4.1 变量声明规范


// ✅ 推荐:明确类型

String name = "";

List<String> items = [];

  


// ✅ 推荐:确实可能为空时使用可空类型

String? errorMessage;

  


// ❌ 避免:滥用可空类型

String? name;  // 如果 name 永远不会为 null,不要用 ?

3.2 函数参数规范


// ✅ 推荐:必需参数使用非空类型

void greet(String name) {

  print("Hello, $name");

}

  


// ✅ 推荐:可选参数使用可空类型或默认值

void greet2({String? name}) {

  print("Hello, ${name ?? 'Guest'}");

}

  


void greet3({String name = "Guest"}) {

  print("Hello, $name");

}

  


// ✅ 推荐:required 关键字用于必需的命名参数

void createUser({required String name, required int age}) {

  // ...

}

3.3 返回值规范


// ✅ 推荐:明确返回值是否可能为空

String getName() {

  return "张三"// 永远返回非空值

}

  


String? findUser(int id) {

  // 可能返回 null(用户不存在)

  return users[id];

}

  


// ✅ 推荐:使用注释说明返回 null 的情况

/// 根据 ID 查找用户

/// 

/// 返回 null 如果用户不存在

String? findUserById(int id) {

  // ...

}

3.4 类成员规范


class User {

  // ✅ 必需字段使用非空类型

  final String name;

  final int age;

  

  // ✅ 可选字段使用可空类型

  final String? avatar;

  final String? bio;

  

  // ✅ 使用 required 确保必需参数被传入

  User({

    required this.name,

    required this.age,

    this.avatar,

    this.bio,

  });

}


第五部分:常见错误与修复

4.1 强制解包 ! 的滥用

❌ 错误示例(来自项目实际代码)

// ai_picture_books.dart 第183行(修复前)

Image.network(picBookList[curPBIndex].pictureUrl!,  // 💥 如果 pictureUrl 为 null 会崩溃

    errorBuilder: (context, error, stackTrace) {

      return Container(...);  // errorBuilder 永远不会执行

    }

)

问题pictureUrl! 的强制解包在 Image.network() 被调用之前执行,如果为 null 会直接崩溃,errorBuilder 根本不会执行。

✅ 正确做法

// ai_picture_books.dart 第185-186行(修复后)

Image.network(

    picBookList[curPBIndex].pictureUrl ?? ''// 使用空字符串代替 null

    errorBuilder: (context, error, stackTrace) {

      return Container(...);  // 空字符串会触发 errorBuilder

    }

)

4.2 未检查 mounted 的异步回调

❌ 错误示例

// 异步回调中直接调用 setState

audioPlayer.onPlayerStateChanged.listen((PlayerState state) {

  setState(() {  // 💥 如果页面已销毁,会报错

    isPlaying = state == PlayerState.playing;

  });

});

✅ 正确做法

audioPlayer.onPlayerStateChanged.listen((PlayerState state) {

  if (!mounted) return// ✅ 先检查页面是否还存在

  setState(() {

    isPlaying = state == PlayerState.playing;

  });

});

4.3 数组越界访问

❌ 错误示例

// 直接访问数组元素,未检查边界

playAsyncAudio(picBookList[curPBIndex].audioUrl);

✅ 正确做法

// 先检查数组是否为空和索引是否有效

if (picBookList.isNotEmpty && curPBIndex < picBookList.length) {

  playAsyncAudio(picBookList[curPBIndex].audioUrl);

}

4.4 网络请求回调中的空安全

❌ 错误示例

HttpUtil().get(url).then((value) {

  setState(() {  // 💥 页面可能已销毁

    data = value.data;

  });

});

✅ 正确做法

HttpUtil().get(url).then((value) {

  if (!mounted) return// ✅ 检查 mounted

  if (value.data == null) return// ✅ 检查数据

  setState(() {

    data = value.data!;

  });

});

4.5 JSON 解析中的空安全

❌ 错误示例

class User {

  final String name;

  

  User.fromJson(Map<String, dynamic> json)

      : name = json['name'];  // 💥 如果 json['name'] 为 null 会崩溃

}

✅ 正确做法

class User {

  final String name;

  

  User.fromJson(Map<String, dynamic> json)

      : name = json['name'] ?? ''// ✅ 提供默认值

      

  // 或者使用可空类型

  // final String? name;

  // User.fromJson(Map<String, dynamic> json) : name = json['name'];

}


第六部分:代码审查清单

在代码审查时,请检查以下要点:

✅ 变量声明

  • 是否正确区分可空和非空类型?

  • 是否避免了不必要的可空类型?

  • late 变量是否在使用前一定会被初始化?

✅ 强制解包 !

  • 是否有充分理由使用 !

  • 使用 ! 前是否已确认值不为 null?

  • 是否可以用 ???. 代替?

✅ 异步回调

  • setState 前是否检查了 mounted

  • 网络请求回调是否处理了页面销毁的情况?

  • Timer 回调是否检查了 mounted

✅ 集合操作

  • 访问数组元素前是否检查了边界?

  • 是否检查了集合是否为空?

✅ 函数参数

  • 必需参数是否使用了 required

  • 可选参数是否有合理的默认值?

✅ dynamic 类型

  • 是否有不必要的 dynamic 类型?

  • JSON 解析后是否进行了类型转换?

  • Map<String, dynamic> 的值是否在使用前进行了类型检查?


附录:快速参考

| 操作符 | 用途 | 示例 |

|--------|------|------|

| ? | 声明可空类型 | String? name; |

| ?. | 安全调用 | name?.length |

| ?? | 空值合并 | name ?? "默认" |

| ??= | 空值赋值 | name ??= "默认" |

| ! | 强制解包(谨慎) | name!.length |

| late | 延迟初始化 | late String name; |

| required | 必需参数 | {required String name} |

| dynamic | 动态类型(谨慎) | 避免使用,用 Object? 代替 |

| as | 类型转换 | json['name'] as String? |

| is | 类型检查 | if (data is String) |