跳到主要內容

泛型

如果你檢視基本陣列型別 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)。