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 保留类型关系时,仍应优先依靠静态类型,而不是大量运行时检查。
泛型类型之间不是简单替换关系
即使 Cat 是 Animal 的子类型,也不能因此忽略集合允许写入的元素类型:
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
}
| 对比项 | Dart | TypeScript |
|---|---|---|
| 类型参数语法 | 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 | null、T | 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只应留给确实需要动态行为的边界。