这句其实就是一句话:
Dart 的 泛型 在运行时会把「可空 / 不可空」的信息弄丢,导致你以为是安全的非空 List,结果偷偷塞进去了 null ,直接崩溃。
1. 先看正常情况(你期望的)
你写了一个非空字符串列表:
List<String> list = [];
String:非空,不能是 nullList<String>:里面只能放字符串,不能放 null
理论上:
list.add(null); // 编译直接报错 ✅ 安全
2. 但泛型一搞,就出事了(可空性擦除)
Dart 在运行时,泛型的 <T> 里面的可空性会被擦掉。
简单说:
- 编译时:知道
String和String?的区别 - 运行时:只知道是 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 泛型可空性擦除怎么避免? 答:
- 给泛型加非空约束
<T extends Object> - 不在泛型方法内手动添加 null
- 需要可空时明确声明
List<T?> - 避免动态类型转泛型时带入 null
- 类字段判断空时先赋值给局部变量,利用类型提升