泛型

优质
小牛编辑
129浏览
2023-12-01

如果你查看基本数组类型 List 的 API 文档,你会发现它的类型其实是 List<E>。<...> 标记表示 List 是一个”泛型“(或带参数的)类——具有形式上的类型参数的类型。按照惯例,类型变量的名字是单个字母,比如 E,T,S,K,和 V。

为什么用泛型?

泛型通常是类型安全的要求,但它们除了让你的代码可以运行外还有诸多益处:

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

如果你只想让一个列表包含字符串,你可以指定它为 List<String>(读作“字符串列表”)。这样一来,你、你的同事和你的工具可以检测到将一个非字符串对象添加到该列表是错误的。下面是一个例子:

var names = List<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 是替身类型。它是一个占位符,你可以将其视为开发者稍后定义的类型。

使用集合字面量

列表和映射字面量可以是参数化的。参数化的字面量就像你之前见过的字面量,只是在左括号前加上了 <type>(对于列表)或 <keyType, valueType>(对于映射)。下面是一个使用类型字面量的例子:

var names = <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 names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = Set<String>.from(names);

下面的代码创建了一个有整数键和 View 类型值的映射:

var views = Map<int, View>();

泛型集合和它们包含的类型

Dart 的泛型类是“实体化”的,这意味着它们在运行期携带了自己的类型信息。因此,你可以检测一个集合的类型:

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

说明:作为对照,Java 中的泛型使用“擦除”,意味着泛型信息在运行时被移除。在 Java 中,可以检测一个对象是否是一个 List,但是你不能检测它是否是一个 List<String>。

限制参数类型

当实现一个泛型时,你可能想要限制它的参数类型。你可以使用 extends 做到这点。

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); // 'Foo<SomeBaseClass>' 的实例

指定任意非 SomeBaseClass 类型会得到一个错误:

var foo = Foo<Object>();

使用泛型方法

起初,Dart 的泛型支持仅限于类。一个新的语法,称为“泛型方法“,允许在方法上使用类型参数:

T first<T>(List<T> ts) {
  // 做一些初始化工作或者错误检查
  T tmp = ts[0];
  // 做一些额外的检查或处理
  return tmp;
}

这里 first (<T>) 中的泛型参数允许你在以下几个地方使用类型参数 T

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

要了解关于泛型的更多信息,请参阅 使用泛型方法