如果你查看基础数组类型 List 的 API 文档,会发现该类型实际为 List<E>。<...> 符号表明 List 是泛型类型(或参数化类型)—— 一种具有形式类型参数的类型。按照惯例,大多数类型变量使用单个字母命名,如 E、T、S、K 和 V。
为何使用泛型?
泛型通常是类型安全所必需的,但它的优势不仅限于让代码运行,还包括:
- 正确指定泛型类型可生成更优的代码。
- 可利用泛型减少代码重复。
如果你希望列表仅包含字符串,可将其声明为 List<String>(读作“字符串列表”)。这样一来,你、其他程序员以及工具都能检测到向列表中赋值非字符串可能是一个错误。示例如下:
✗ 静态分析:失败
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 报错使用泛型的另一个原因是减少代码重复。泛型允许你在多种类型之间共享单个接口和实现,同时仍能利用静态分析。例如,假设你为缓存对象创建一个接口:
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}后来你发现需要该接口的字符串特定版本,于是创建了另一个接口:
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}再后来,你可能又想为数字创建特定版本的接口……你懂的。
泛型类型可避免创建所有这些接口的麻烦。相反,你可以创建一个带类型参数的单一接口:
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}在此代码中,T 是替代类型,它是一个占位符,可将其视为开发人员稍后将定义的类型。
使用集合字面量
List、Set 和 Map 字面量可以进行参数化。参数化字面量与你已见过的字面量类似,只不过在开括号前添加 <type>(用于列表和集合)或 <keyType, valueType>(用于映射)。以下是使用类型化字面量的示例:
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',
};在构造函数中使用参数化类型
若要在使用构造函数时指定一个或多个类型,需在类名后使用尖括号 <...> 包含类型。例如:
var nameSet = Set<String>.of(names);以下代码创建了一个键为整数、值为 View 类型的 SplayTreeMap:
var views = SplayTreeMap<int, View>();泛型集合及其包含的类型
Dart 泛型类型是具体化的,这意味着它们在运行时会携带类型信息。例如,你可以测试集合的类型:
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true注意
相比之下,Java 中的泛型使用擦除机制,这意味着泛型类型参数在运行时会被移除。在 Java 中,你可以测试对象是否为 List,但无法测试它是否为 List<String>。
限制参数化类型
实现泛型类型时,可能需要限制可作为参数提供的类型,使参数必须是特定类型的子类型。这种限制称为边界,可使用 extends 关键字实现。
一个常见用例是通过使类型成为 Object 的子类型(而非默认的 Object?)来确保类型为非空。
class Foo<T extends Object> {
// 为 Foo 提供的任何 T 类型都必须是非空的。
}除了 Object,你还可以对其他类型使用 extends。以下示例扩展了 SomeBaseClass,因此可以对 T 类型的对象调用 SomeBaseClass 的成员:
class Foo<T extends SomeBaseClass> {
// 实现代码在此...
String toString() => "Instance of 'Foo<$T>'";
}
class Extender extends SomeBaseClass {
...
}将 SomeBaseClass 或其任何子类型作为泛型参数是允许的:
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();不指定泛型参数也可行:
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'指定任何非 SomeBaseClass 类型会导致错误: ✗ 静态分析:失败
var foo = Foo<Object>();自引用类型参数限制(F 边界)
使用边界限制参数类型时,可以将边界引用回类型参数本身。这会创建自引用约束,即 F 边界。例如:
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;
}
var useIt = compareAndOffset(A(), A());F 边界 T extends Comparable<T> 表示 T 必须能与自身比较。因此,A 只能与相同类型的其他实例进行比较。
使用泛型方法
方法和函数也允许类型参数:
T first<T>(List<T> ts) {
// 执行一些初始化工作或错误检查,然后...
T tmp = ts[0];
// 执行一些额外的检查或处理...
return tmp;
}此处 first 上的泛型类型参数 <T> 允许在以下多个位置使用类型参数 T:
- 函数的返回类型(
T)。 - 参数的类型(
List<T>)。 - 局部变量的类型(
T tmp)。