跳至主要內容

泛型

如果您查看基本陣列類型 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 是替代類型。 它是一個佔位符,您可以將其視為開發人員稍後將定義的類型。

使用集合常值

#

列表、集合和映射常值可以參數化。 參數化常值就像您已經看過的常值一樣,只是您在左括號之前新增 <type>(對於列表和集合)或 <keyType, valueType>(對於映射)。 以下是使用類型化常值的範例

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 泛型類型是具體化的,這表示它們在執行時攜帶其類型資訊。 例如,您可以測試集合的類型

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

限制參數化類型

#

實作泛型類型時,您可能想要限制可以作為引數提供的類型,以便引數必須是特定類型的子類型。 此限制稱為界限。 您可以使用 extends 來執行此操作。

常見的用例是透過將類型設為 Object 的子類型(而不是預設值 Object?)來確保類型不可為空。

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

除了 Object 之外,您還可以將 extends 與其他類型一起使用。 以下是擴展 SomeBaseClass 的範例,以便可以在 T 類型的物件上呼叫 SomeBaseClass 的成員

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-bounds)

#

當使用界限來限制參數類型時,您可以將界限參照回類型參數本身。 這會建立自我參照約束,或稱為 F-bound。 例如

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-bound 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) 中。