Generics

Generics

如果您查看基础的列表类型 List 的 API 文档,您会看到其类型实际上是 List<E><...> 符号将 List 标记为一种泛型(或参数化)类型——即一个拥有形式类型参数的类型。按照约定,大多数类型变量都使用单字母名称,例如 E、T、S、K 和 V。

为什么要使用泛型?

泛型通常是类型安全所必需的,但它们的好处不仅仅是让您的代码能够运行:

  • 正确地指定泛型可以产生质量更高的生成代码。
  • 您可以使用泛型来减少代码重复。

如果您希望一个列表只包含字符串,您可以将其声明为 List<String>(读作“字符串列表”)。这样,您、您的同事以及您的工具都能检测到将一个非字符串赋值给该列表可能是一个错误。下面是一个例子:

1
2
3
4
static analysis: failure
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 错误

使用泛型的另一个原因是为了减少代码重复。泛型让您可以在多种类型之间共享同一个接口和实现,同时仍然能利用静态分析的优势。例如,假设您创建了一个用于缓存对象的接口:

1
2
3
4
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}

然后您发现需要一个专门针对字符串的版本,于是您创建了另一个接口:

1
2
3
4
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}

后来,您又决定需要一个专门针对数字的版本……以此类推。

泛型可以省去您创建所有这些接口的麻烦。您可以只创建一个接受类型参数的接口:

1
2
3
4
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}

在这段代码中,T 是一个待定类型。它是一个占位符,您可以将其理解为稍后由开发者定义的类型。

使用集合字面量

List、set 和 map 的字面量可以是参数化的。参数化字面量与您已经见过的字面量一样,只是您需要在开括号前添加 <type>(对于 list 和 set)或 <keyType, valueType>(对于 map)。以下是使用类型化字面量的示例:

1
2
3
4
5
6
7
var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
'index.html': 'Homepage',
'robots.txt': 'Hints for web robots',
'humans.txt': 'We are people, not machines',
};

结合构造函数使用参数化类型

在使用构造函数时,要指定一个或多个类型,请将类型放在类名后面的尖括号(<...>)中。例如:

1
var nameSet = Set<String>.of(names);

以下代码创建了一个 SplayTreeMap,其键为整数,值为 View 类型:

1
var views = SplayTreeMap<int, View>();

泛型集合及其包含的类型

Dart 的泛型是实化的(reified),这意味着它们在运行时会携带自己的类型信息。例如,您可以测试一个集合的类型:

1
2
3
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

注意
相比之下,Java 中的泛型使用了擦除(erasure)技术,这意味着泛型类型参数在运行时会被移除。在 Java 中,您可以测试一个对象是否是 List,但您无法测试它是否是 List<String>

限制参数化类型

在实现泛型类型时,您可能希望限制那些可以作为参数提供的类型,使其必须是某个特定类型的子类型。这种限制被称为类型约束 (bound)。您可以使用 extends 关键字来实现这一点。

一个常见的用例是通过使其成为 Object 的子类型(而不是默认的 Object?)来确保一个类型是不可空的。

1
2
3
class Foo<T extends Object> {
// 提供给 Foo 的任何 T 类型都必须是不可空的。
}

除了 Object,您也可以使用 extends 关键字与其他类型一起进行约束。下面是一个扩展 SomeBaseClass 的例子,这样 SomeBaseClass 的成员就可以在类型为 T 的对象上被调用:

1
2
3
4
5
6
7
8
class Foo<T extends SomeBaseClass> {
// 实现代码...
String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {
...
}

使用 SomeBaseClass 或其任何子类型作为泛型参数都是可以的:

1
2
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

不指定泛型参数也是可以的:

1
2
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

指定任何非 SomeBaseClass 的类型都会导致错误:

1
2
static analysis: failure
var foo = Foo<Object>();

自引用类型参数约束(F-bounds)

当使用类型约束来限制参数类型时,您可以将约束引用回类型参数自身。这就创建了一个自引用约束,即 F-bound。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract interface class Comparable<T> {
int compareTo(T o);
}

int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
t1.compareTo(t2) + 1;

class A implements Comparable<A> {
@override
int compareTo(A other) => /*...实现...*/ 0;
}

int useIt = compareAndOffset(A(), A());

F-bound T extends Comparable<T> 意味着 T 必须能与它自己进行比较。因此,A 只能与相同类型的其他实例进行比较。

使用泛型方法

方法和函数也允许使用类型参数:

1
2
3
4
5
6
T first<T>(List<T> ts) {
// 做一些初始工作或错误检查,然后...
T tmp = ts[0];
// 做一些额外的检查或处理...
return tmp;
}

在这里,first 方法上的泛型类型参数(<T>)允许您在多个地方使用类型参数 T

  • 在函数的返回类型中(T)。
  • 在参数的类型中(List<T>)。
  • 在局部变量的类型中(T tmp)。
作者

wuhunyu

发布于

2025-09-04

更新于

2025-09-11

许可协议