Dart - 泛型的可空性擦除

3 阅读3分钟

这句其实就是一句话:

Dart 的 泛型 在运行时会把「可空 / 不可空」的信息弄丢,导致你以为是安全的非空 List,结果偷偷塞进去了 null ,直接崩溃。


1. 先看正常情况(你期望的)

你写了一个非空字符串列表

List<String> list = [];
  • String:非空,不能是 null
  • List<String>:里面只能放字符串,不能放 null

理论上:

list.add(null); // 编译直接报错 ✅ 安全

2. 但泛型一搞,就出事了(可空性擦除)

Dart 在运行时,泛型的 <T> 里面的可空性会被擦掉

简单说:

  • 编译时:知道 StringString? 的区别
  • 运行时:只知道是 Object,不知道能不能为空

这就叫 可空性擦除(nullability erasure)


3. 最经典的坑:泛型方法里塞 null

看这段代码,编译不报错,运行直接空指针

void addNullToList<T>(List<T> list) {
  list.add(null); // 编译不报错!
}

然后你调用:

List<String> myList = []; // 非空列表

addNullToList(myList); // 把 null 塞进去了!

结果:

  • 编译:通过
  • 运行:列表里出现了 null
  • 下次一用 myList.first 直接崩溃

这就是你那句话的意思:

List 中 T 为非空类型,但运行时插入了 null。


4. 为什么会这样?

因为 Dart 运行时看不到 T 到底是不是可空

它只看到:

  • List
  • 不知道是 List<String> 还是 List<String?>

所以它允许你 add(null),

但你实际用的时候就炸了。


5. 极简总结

**Dart 泛型在运行时会丢失「可空 / 不可空」的类型信息,

导致编译时看起来安全的非空泛型集合,

运行时可能被偷偷塞入 null,从而触发空指针异常。**

核心原则

Dart 泛型运行时可空性会被擦除,所以:

不要在泛型方法里随便加 null,也不要信任泛型能自动拦住 null。


1. 最有效:给泛型加上 非空约束

void addSafe<T extends Object>(List<T> list, T value) {
  list.add(value);
}

关键点:

  • <T extends Object> = T 一定是非空
  • 这时你再想传 null编译直接报错
  • 从根源上杜绝运行时塞 null

这是官方推荐、最安全的写法。


2. 绝对不要在泛型方法里手动 add(null)

// 危险!!
void addNull<T>(List<T> list) {
  list.add(null); // 编译不报错,运行炸
}

只要你不自己写这种代码,90% 的泛型空安全问题都不会出现。


3. 尽量用 List<T?> 明确表示可空,不要模糊

如果你真的需要存 null,就明确写可空泛型

List<String?> list = [];

这样别人一看就知道:

  • 这里能存 null
  • 使用时自己判断 if (item != null)

模糊的泛型最容易出事。


4. 尽量避免“泛型桥接”传递可空性

比如从动态类型、动态 List 转成泛型 List:

List<dynamic> dynamicList = [1, null, "aaa"];

List<String> realList = List.from(dynamicList);

这种最容易把 null 带进去。

避免方式:

  • 过滤 null
List<String> realList = dynamicList
    .where((e) => e != null)
    .cast<String>()
    .toList();

5. 类字段(instance variable)不会类型提升,必须小心

这个是 Dart 第二大坑。

class Demo {
  String? name;

  void test() {
    if (name != null) {
      print(name.length); // 报错!
    }
  }
}

为什么?

因为类字段可能被别的线程/方法改掉,Dart 不敢提升。

解决:赋值给局部变量

void test() {
  final local = name;
  if (local != null) {
    print(local.length); // 正常
  }
}

6. 常见问题

问:Dart 泛型可空性擦除怎么避免? 答:

  1. 给泛型加非空约束 <T extends Object>
  2. 不在泛型方法内手动添加 null
  3. 需要可空时明确声明 List<T?>
  4. 避免动态类型转泛型时带入 null
  5. 类字段判断空时先赋值给局部变量,利用类型提升