跳到主要内容

Generics(泛型)

泛型允许类、函数和类型别名把一部分类型留作参数,在使用时再传入具体类型。它可以复用同一套实现,同时保留静态类型检查,避免为了支持多种数据而退回到 dynamic

class Box<T> {
Box(this.value);

T value;
}

void main() {
final nameBox = Box<String>('Dart');
final scoreBox = Box<int>(95);

print(nameBox.value.toUpperCase()); // DART
print(scoreBox.value + 5); // 100
}

这里的 T 是类型参数。创建 Box<String> 时,类中的 T 表示 String;创建 Box<int> 时,T 表示 int。因此,同一个 Box<T> 可以保存不同类型的数据,静态分析器仍然知道每个 value 的具体类型。

为什么使用泛型

不使用泛型时,可以用 Object? 保存任意值,但读取值后通常需要检查或转换类型:

class ObjectBox {
ObjectBox(this.value);

Object? value;
}

void main() {
final box = ObjectBox('Dart');
final value = box.value;

if (value is String) {
print(value.toUpperCase());
}
}

如果改用 dynamic,虽然可以直接调用成员,但错误可能推迟到运行时:

class DynamicBox {
DynamicBox(this.value);

dynamic value;
}

void main() {
final box = DynamicBox(42);

// 静态分析无法发现此处的问题,运行时会抛出错误。
// print(box.value.toUpperCase());
}

泛型同时解决了这两个问题:实现只需要编写一次,调用者又能得到具体的静态类型。类型不匹配时,错误可以在运行程序前被发现:

void main() {
final box = Box<String>('Dart');

// box.value = 42; // 编译错误:int 不能赋值给 String
box.value = 'Generics';
print(box.value);
}

泛型类

在类名后使用尖括号声明类型参数。类型参数可以用于字段、构造函数参数、方法参数和返回值:

class Store<T> {
Store(this._items);

final List<T> _items;

void add(T item) {
_items.add(item);
}

T get first => _items.first;

List<T> get all => List<T>.unmodifiable(_items);
}

void main() {
final topics = Store<String>(['types', 'records']);
topics.add('generics');

print(topics.first); // types
print(topics.all); // [types, records, generics]
}

Store<String> 会把类内所有 T 替换为 String。因此,add 只接受字符串,first 返回字符串,all 的类型是 List<String>

一个泛型声明也可以有多个类型参数。常用命名约定包括:

  • T:某个普通类型;
  • E:集合中的元素类型;
  • K:键的类型;
  • V:值的类型;
  • R:函数返回值类型。

这些名称只是约定,不会改变语言行为。含义不够明确时,可以使用更完整的名称。

class Pair<First, Second> {
const Pair(this.first, this.second);

final First first;
final Second second;
}

void main() {
const result = Pair<String, int>('Generics', 100);

print(result.first); // Generics
print(result.second); // 100
}

泛型函数和泛型方法

函数也可以声明自己的类型参数。类型参数写在函数名之后、参数列表之前:

T firstOf<T>(List<T> values) {
if (values.isEmpty) {
throw StateError('values cannot be empty');
}

return values.first;
}

void main() {
final topic = firstOf<String>(['types', 'generics']);
final score = firstOf<int>([90, 95]);

print(topic.toUpperCase()); // TYPES
print(score + 5); // 95
}

泛型函数可以表达“参数类型与返回类型之间保持某种关系”。例如,下面的函数接收 T,并返回 List<T>

List<T> repeat<T>(T value, int count) {
return List<T>.filled(count, value);
}

void main() {
final topics = repeat('Dart', 3); // List<String>
final scores = repeat(100, 2); // List<int>

print(topics); // [Dart, Dart, Dart]
print(scores); // [100, 100]
}

类中的方法也能声明独立于类的类型参数:

class Converter<Input> {
const Converter(this.value);

final Input value;

Output convert<Output>(Output Function(Input) transform) {
return transform(value);
}
}

void main() {
const converter = Converter<int>(42);
final label = converter.convert<String>((value) => 'score: $value');

print(label); // score: 42
}

这里的 Input 属于类,Output 只属于 convert 方法,二者的作用域不同。

类型推断

Dart 通常可以根据构造函数参数、函数实参或赋值上下文推断类型参数,因此不必每次都显式书写:

class Box<T> {
Box(this.value);

T value;
}

T identity<T>(T value) => value;

void main() {
final box = Box('Dart'); // Box<String>
final score = identity(95); // int

print(box.value.toUpperCase());
print(score + 5);
}

当上下文不足、推断结果过宽,或公共 API 需要明确表达意图时,可以显式提供类型参数:

void main() {
final emptyTopics = <String>[];
final nullableBox = Box<String?>(null);
final result = identity<num>(42);

print(emptyTopics.runtimeType);
print(nullableBox.value);
print(result);
}

显式类型参数不是越多越好。局部变量的类型明显时可以依赖推断;空集合、可空类型和对外接口则通常值得写清楚。

类型约束:extends

默认情况下,类型参数可以接收很宽泛的类型。需要使用某个类型提供的成员,或需要排除不符合要求的类型时,可以使用 extends 指定上界(upper bound):

class Ranked<T extends num> {
const Ranked(this.value);

final T value;

bool isHigherThan(T other) => value > other;
}

void main() {
const integerRank = Ranked<int>(95);
const decimalRank = Ranked<double>(9.5);

print(integerRank.isHigherThan(90)); // true
print(decimalRank.isHigherThan(10.0)); // false

// Ranked<String>('high'); // 编译错误:String 不是 num 的子类型
}

因为 T 被限制为 num 或其子类型,类内部可以安全使用 num 提供的比较运算。约束也可以指向自定义类:

abstract class Identifiable {
String get id;
}

class Lesson implements Identifiable {
Lesson(this.id, this.title);


final String id;

final String title;
}

T findById<T extends Identifiable>(List<T> items, String id) {
return items.firstWhere((item) => item.id == id);
}

void main() {
final lessons = [
Lesson('types', 'Built-in types'),
Lesson('generics', 'Generics'),
];

final lesson = findById(lessons, 'generics'); // Lesson
print(lesson.title); // Generics
}

findById 既能使用 Identifiable.id,又能保留调用者传入的具体类型 Lesson。如果只把参数和返回值写成 Identifiable,返回结果就会丢失 Lesson 的静态类型信息。

在空安全代码中,T extends Object 还可以明确排除可空类型:

T requireValue<T extends Object>(T value) => value;

void main() {
print(requireValue<String>('Dart'));

// requireValue<String?>(null); // 编译错误:String? 不满足 Object 上界
}

泛型与空安全

可空标记属于类型的一部分,因此 Box<String>Box<String?> 是不同的泛型类型:

class Box<T> {
Box(this.value);

T value;
}

void main() {
final requiredName = Box<String>('Dart');
final optionalName = Box<String?>(null);

// requiredName.value = null; // 编译错误
optionalName.value = 'Generics';

print(requiredName.value);
print(optionalName.value);
}

对于没有显式上界的类型参数,不要假定值一定非空。若实现必须直接使用非空 Object 的能力,应通过 T extends Object 表达约束,而不是在方法内部使用强制解包来掩盖类型问题。

泛型类型在运行时可用

Dart 会在运行时保留泛型类型参数。这个特性通常称为泛型具体化(reified generics)。因此,可以检查对象是否为某个带具体类型参数的泛型类型:

void main() {
final names = <String>['Ada', 'Lin'];

print(names is List<String>); // true
print(names is List<int>); // false
print(names.runtimeType); // 具体文本由运行时实现决定
}

不要依赖 runtimeType.toString() 的具体输出,它不属于稳定的业务接口。需要分支判断时,应使用 is;需要让 API 保留类型关系时,仍应优先依靠静态类型,而不是大量运行时检查。

泛型类型之间不是简单替换关系

即使 CatAnimal 的子类型,也不能因此忽略集合允许写入的元素类型:

class Animal {
const Animal(this.name);

final String name;
}

class Cat extends Animal {
const Cat(super.name);
}

class Dog extends Animal {
const Dog(super.name);
}

void main() {
final cats = <Cat>[const Cat('Milo')];
List<Animal> animals = cats;

// 静态类型允许 Animal,但实际列表仍然是 List<Cat>。
// animals.add(const Dog('Rex')); // 运行时会抛出类型错误

print(animals.first.name);
}

Dart 的泛型类类型参数是协变的,所以 List<Cat> 可以赋给 List<Animal>。但对象在运行时仍是 List<Cat>,写入 Dog 会违反它的实际元素类型并抛出错误。

如果函数只需要读取元素,可以接收 Iterable<Animal>,减少误写的机会;如果函数需要写入,应让参数类型准确反映可以加入的元素,并避免把较窄类型的可变集合当作较宽类型使用。

与 TypeScript 的关键差异

Dart 和 TypeScript 都使用尖括号声明泛型,也都支持泛型类、泛型函数和 extends 约束:

class Box<T> {
Box(this.value);

T value;
}

T identity<T>(T value) => value;
class Box<T> {
constructor(public value: T) {}
}

function identity<T>(value: T): T {
return value
}
对比项DartTypeScript
类型参数语法Box<T>identity<T>Box<T>identity<T>
类型推断根据实参和上下文推断同样会根据实参和上下文推断
运行时类型参数泛型类型参数会保留,可检查 value is List<String>类型参数编译后通常被擦除,不能直接检查 value instanceof Array<string>
非空上界T extends Object 可排除可空类型T extends object 的含义受 TypeScript 类型系统规则影响,不应视为完全等价
可空泛型参数使用 T? 或传入 String? 等可空类型通常使用 T | nullT | undefined

二者的泛型语法相似,但运行时模型不同。尤其不要把 TypeScript 中“类型参数只存在于编译期”的经验直接套用到 Dart。

常见误区

误区一:用 dynamic 代替泛型

dynamic 会让许多检查推迟到运行时。参数与返回值之间存在稳定类型关系时,应使用类型参数表达这种关系。

误区二:认为类型参数只能用于集合

List<T>Set<T>Map<K, V> 是最常见的泛型类型,但自定义类、函数、方法和类型别名同样可以声明类型参数。

误区三:在类的静态成员中使用类的类型参数

静态成员属于类本身,不属于某个具体的 Box<String>Box<int>,所以不能直接使用类的类型参数 T

class Box<T> {
Box(this.value);

final T value;

// static T? cachedValue; // 编译错误:静态成员不能引用 T
}

如果静态方法本身需要泛型,可以为该方法声明独立的类型参数。

误区四:依赖 runtimeType 的字符串结果

Dart 保留泛型类型信息,但 runtimeType.toString() 的格式不应作为程序逻辑依据。使用 is 做类型判断,或通过泛型 API 保留静态类型。

误区五:忽略可变泛型集合的写入风险

List<Cat> 作为 List<Animal> 使用后,不能安全地加入任意 Animal。设计 API 时要区分只读遍历和写入操作,并让类型尽可能准确。

小结

  • 泛型通过类型参数复用类和函数实现,同时保留静态类型检查。
  • 类型参数可以用于类、函数、方法和类型别名,一个声明也可以有多个类型参数。
  • Dart 能根据构造参数、函数实参和上下文推断泛型参数;空集合或意图不明确时应显式标注。
  • T extends Base 为类型参数设置上界,使实现可以安全使用上界提供的能力。
  • T extends Object 可以在空安全代码中排除可空类型。
  • Dart 的泛型类型参数在运行时保留,但不应依赖 runtimeType.toString() 的具体文本。
  • Dart 泛型类的类型参数是协变的;对可变集合进行宽化赋值后,错误写入仍可能在运行时失败。
  • 泛型适合表达稳定的类型关系,dynamic 只应留给确实需要动态行为的边界。