文章

Dart 泛型深度解析:从类型擦除到 Reified 泛型,面试官想听什么?

Dart 泛型面试向解读:从类型擦除到 Reified 泛型(运行时保留类型),讲清泛型函数、泛型类与约束(extends)。 掌握后能答出 Dart 与普通 JVM 语言泛型的本质差异。

Dart 泛型深度解析:从类型擦除到 Reified 泛型,面试官想听什么?

一句话概括

Dart 泛型在编译时保证类型安全,而且运行时保留类型信息(Reified)——这是它和 Java/TypeScript 最大的不同,也是 Flutter 中 List<int> is List<num> 能正确工作的根本原因。

核心知识点

1. 泛型类:类型安全的容器

1
2
3
4
5
6
7
8
class Box<T> {
  T value;
  Box(this.value);
}

final intBox = Box<int>(42);
// intBox.value = 'hello'; // ❌ 编译错误
final strBox = Box('hi');  // 类型推断 → Box<String>

面试要点:类型参数可以从构造函数实参自动推断,不用显式写 <String>。

2. 泛型约束:限制类型范围的 <T extends X>

1
2
3
4
5
6
7
8
9
10
11
12
// T 必须实现 Comparable,才能调用 compareTo
class SortedList<T extends Comparable<T>> {
  final List<T> _items = [];
  void add(T item) {
    _items.add(item);
    _items.sort((a, b) => a.compareTo(b));
  }
}

final nums = SortedList<int>()..add(3)..add(1)..add(2);
print(nums.items); // [1, 2, 3]
// SortedList<Object>(); // ❌ Object 没有实现 Comparable

Dart 泛型约束只用 extends,不管后面是类还是接口。如果需要多约束,让具体类同时实现多个接口。

3. Reified 泛型:运行时类型不擦除

这就是 Dart 泛型的杀手锏:

1
2
3
4
5
6
7
8
9
10
11
final list = <int>[1, 2, 3];
print(list is List<int>);   // true ✅ — Java 做不到
print(list is List<String>);// false ✅ — 运行时也知道类型不同
print(list.runtimeType);    // List<int> — 不是擦除后的 List

// 可以直接用类型参数做分支
T fromJson<T>(Map<String, dynamic> json) {
  if (T == User) return User.fromJson(json) as T;
  if (T == Product) return Product.fromJson(json) as T;
  throw ArgumentError('Unknown type: $T');
}
语言List<T> 运行时is List<int>
DartList<int>✅ 正确
JavaArrayList(擦除)❌ 编译错误
TypeScript无运行时类型❌ 不存在

4. 泛型方法:类型推断链路

1
2
3
4
5
T first<T>(List<T> items) => items.first;

final n = first([1, 2, 3]);          // T = int,从参数推断
// 不是从返回值上下文推断的
List<String> x = first(['a', 'b']);  // T = String,还是从参数推断

推断规则优先级:显式指定 > 参数类型 > 返回类型上下文。和 TypeScript 不一样,Dart 不会为了匹配赋值目标而改变泛型推断。

5. 协变带来的陷阱

Dart 的 List 是协变的:List<Cat> 可以赋值给 List<Animal>:

1
2
3
4
List<Cat> cats = [Cat()];
List<Animal> animals = cats; // ✅ 协变,编译通过
animals.add(Dog());          // ✅ 编译通过!运行时也不报错
// 现在 cats 里混进了 Dog 😱

解决方案:用 List.unmodifiable() 创建不可变列表,或者写的时候用具体类型。


其实你每天都在用

  • List<int> / Map<String, dynamic>:Dart 集合库全都是泛型,jsonDecode 返回的 Map<String, dynamic> 就是典型
  • Future<T>:网络请求返回 Future<User>,类型安全地拿到 User 对象
  • State<T extends StatefulWidget>:Flutter 中 State<T> 能安全访问 widget 属性,背后就是泛型约束
  • Provider.of<T>(context):泛型让 Provider 知道你要取哪个类型的状态
  • Freezed / json_serializable:代码生成依赖 Reified 泛型做 JSON 反序列化的类型保护

常见误解(FAQ)

❌ 误区:「Dart 泛型和 Java 泛型一样的」

不一样。Java 用类型擦除,运行时 List<String> 就是 List。Dart 是 Reified,运行时保留了 List<String> 的完整类型信息,is 检查能正确区分。

❌ 误区:「T extends A & B 是合法的泛型约束」

Dart 不支持 & 语法。只能用 extends 接单个约束,多约束时让具体类实现多个接口:

1
2
3
4
abstract class A {}
abstract class B {}
class C implements A, B {}
// 然后写 class Container<T extends C>

❌ 误区:「协变总是安全的」

Dart 的 List 协变是不安全的协变——List<Cat> 能赋值给 List<Animal>,之后往里面 add(Dog) 不会触发运行时错误(和 Java 数组的行为类似)。这不是 bug,是 Dart 刻意为之的设计取舍。

❌ 误区:「泛型方法可以靠返回值类型来推断类型参数」

不能。Dart 从参数推断,不是从赋值上下文推断。这和 TypeScript 的关键区别。


一句话总结

Dart 泛型 = 编译时类型安全 + 运行时类型不擦除(Reified),让你能写出更安全的代码,也能在面试里吊打不会的候选人。

本文由作者按照 CC BY 4.0 进行授权