跳到主要内容

泛型

如果你查看基本数组类型 List 的 API 文档,你会发现其类型实际上是 List<E>。<...> 符号表示 List 是一个 泛型(或 参数化)类型——一个具有形式类型参数的类型。按照惯例,大多数类型变量使用单个字母命名,例如 E、T、S、K 和 V。

为何使用泛型?

#

泛型通常是类型安全的必需,但其好处远不止于让你的代码运行。

  • 正确指定泛型类型会生成更好的代码。
  • 你可以使用泛型来减少代码重复。

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

✗ 静态分析:失败dart
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

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

dart
abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

你发现你想要这个接口的一个字符串专用版本,所以你创建了另一个接口

dart
abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

后来,你决定想要这个接口的一个数字专用版本……你明白了。

泛型可以让你省去创建所有这些接口的麻烦。相反,你可以创建一个接受类型参数的单个接口

dart
abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

在这段代码中,T 是替代类型。它是一个占位符,你可以将其视为开发者稍后将定义的类型。

使用集合字面量

#

List、Set 和 Map 字面量可以参数化。参数化字面量与你已经见过的字面量类似,只不过你在开方括号之前添加了 <type>(用于 List 和 Set)或 <keyType, valueType>(用于 Map)。下面是一个使用类型字面量的例子

dart
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',
};

构造函数中使用参数化类型

#

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

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

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

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

泛型集合及其包含的类型

#

Dart 泛型是具体化的(reified),这意味着它们在运行时保留其类型信息。例如,你可以测试集合的类型

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

限制参数化类型

#

在实现泛型类型时,你可能希望限制可以作为参数提供的类型,以便参数必须是特定类型的子类型。这种限制被称为“界限”(bound)。你可以使用 extends 来实现。

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

dart
class Foo<T extends Object> {
  // Any type provided to Foo for T must be non-nullable.
}

除了 Object 之外,你还可以将 extends 用于其他类型。这是一个扩展 SomeBaseClass 的例子,以便 SomeBaseClass 的成员可以在 T 类型的对象上调用

dart
class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {
  ...
}

SomeBaseClass 或其任何子类型用作泛型参数是允许的

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

不指定泛型参数也是允许的

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

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

✗ 静态分析:失败dart
var foo = Foo<Object>();

自引用类型参数限制 (F-界)

#

当使用界限来限制参数类型时,你可以将界限引用回类型参数本身。这会创建一个自引用约束,或称为 F-界。例如

dart
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) => /*...implementation...*/ 0;
}

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

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

使用泛型方法

#

方法和函数也允许类型参数

dart
T first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

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

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