Dart 泛型

若你查閱基礎陣列型別 List 的 API 文件,會發現它真實的型別其實是 List<E><...> 這種尖括號標記代表這是一個泛型型別(參數化型別),也就是帶有形式型別參數的型別。依照慣例,絕大多數型別變數都使用單一字母命名,例如 ETSKV

為什麼要使用泛型?

  • 泛型最核心的作用是保障型別安全
  • 正確指定泛型可以產生更優化的執行期程式碼;
  • 使用泛型能大幅減少程式碼重複。

如果你希望一個清單只能存放字串,就可以宣告為 List<String>,這就是字串清單

一旦定義這是僅存字串的清單,往裡面放入非字串型別的值就屬於錯誤操作。

// 靜態型別檢查報錯
var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 編譯錯誤:42 不是字串Code language: PHP (php)
D:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:5:12: Error: The argument type 'int' can't be assigned to the parameter type 'String'.
        names.add(42); // 編譯錯誤:42 不是字串Code language: PHP (php)

減少程式碼冗餘

假設你需要一個快取物件的抽象介面:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}
Code language: JavaScript (javascript)

後續你又需要只儲存字串的快取介面,只能重新建立一套:

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

之後還會需要數字專用快取……不斷重複撰寫幾乎一模一樣的程式碼。

舉例來說你可能會寫出這樣的程式:

abstract class IntCache {
  int getByKey(String key);
  void setByKey(String key, int value);
}Code language: Dart (dart)

泛型可以一次性解決這個問題,只需要定義一個帶有型別參數的通用介面:

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

這裡的 T 是佔位型別,代表開發者後續傳入的任意實際型別。

實際範例

// 泛型抽象快取基底類別
abstract class Cache<T> {
  T? getByKey(String key);
  void setByKey(String key, T value);
  void remove(String key);
}

// 記憶體快取實作(底層基於 Map)
class MemoryCache<T> implements Cache<T> {
  final Map<String, T> _storage = {};

  @override
  T? getByKey(String key) {
    return _storage[key];
  }

  @override
  void setByKey(String key, T value) {
    _storage[key] = value;
  }

  @override
  void remove(String key) {
    _storage.remove(key);
  }
}

void main() {
  // 1. 字串快取範例
  final Cache<String> strCache = MemoryCache<String>();
  strCache.setByKey("name", "Tom");
  print(strCache.getByKey("name")); // Tom

  // 2. 整數 int 快取
  final Cache<int> intCache = MemoryCache<int>();
  intCache.setByKey("age", 26);
  print(intCache.getByKey("age")); // 26

  // 3. 自訂物件快取
  final Cache<User> userCache = MemoryCache<User>();
  userCache.setByKey("u1", User("Jack", 18));
  final User? user = userCache.getByKey("u1");
  print(user?.name); // Jack

  // 刪除測試
  userCache.remove("u1");
  print(userCache.getByKey("u1")); // null
}

// 自訂實體類別
class User {
  final String name;
  final int age;
  User(this.name, this.age);
}Code language: Dart (dart)

使用泛型集合字面量

List、Set、Map 字面量都支援參數化泛型:

  • List / Set:在左中括號前方寫 <型別>
  • Map:在左大括號前方寫 <鍵型別, 值型別>

範例:

// 字串清單
var names = <String>['Seth', 'Kathy', 'Lars'];

// 字串集合(自動去重)
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};

// 字串鍵、字串值的字典
var pages = <String, String>{
  'index.html': '首頁',
  'robots.txt': '爬蟲指引檔案',
  'humans.txt': '給人類開發者閱讀的說明檔案',
};
Code language: PHP (php)

執行範例程式

void main() {
  // 1. List<String> 字串清單:有序、允許重複、可透過索引取值
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  print("=== List 清單 ===");
  print(names); // [Seth, Kathy, Lars, Seth] 允許重複
  print(names[0]); // 透過索引取第一個元素:Seth
  names.add("Mike"); // 在末尾新增元素
  print("新增後:$names");

  // 2. Set<String> 字串集合:無序、自動去重、無索引
  var uniqueNames = <String>{'Seth', 'Kathy', 'Lars', 'Seth'};
  print("\n=== Set 集合(自動去重)===");
  print(uniqueNames); // {Seth, Kathy, Lars} 重複的 Seth 自動移除
  print(uniqueNames.contains("Kathy")); // 判斷是否存在:true
  uniqueNames.remove("Lars");
  print("刪除 Lars 後:$uniqueNames");

  // 3. Map<String, String> 鍵值字典:鍵唯一,透過 key 取值
  var pages = <String, String>{
    'index.html': '首頁',
    'robots.txt': '爬蟲指引檔案',
    'humans.txt': '給人類開發者閱讀的說明檔案',
  };
  print("\n=== Map 字典 ===");
  print(pages);
  print(pages['index.html']); // 透過 key 取值:首頁
  pages["about.html"] = "關於頁面"; // 新增鍵值對
  print("新增 about.html 後:$pages");

  // 常用走訪範例
  print("\n=== 走訪 List ===");
  for (var name in names) {
    print("- $name");
  }

  print("\n=== 走訪 Map 所有鍵值 ===");
  for (var entry in pages.entries) {
    print("檔案:${entry.key} → 說明:${entry.value}");
  }
}Code language: Dart (dart)

建構子使用泛型參數

呼叫建構函式時,在類別名稱後方的尖括號內指定泛型型別:

var nameSet = Set<String>.of(names);Code language: JavaScript (javascript)

以下程式建立一個鍵為 int、值為 View 型別的字典:

var views = SplayTreeMap<int, View>();
Code language: HTML, XML (xml)
import 'dart:collection';

void main() {
  var names = <String>['Seth', 'Kathy', 'Lars', 'Seth'];
  var nameSet = Set<String>.of(names);
  print("原始清單:$names");
  print("去重 Set:$nameSet");

  // SplayTreeMap 需要匯入 dart:collection
  var views = SplayTreeMap<int, String>();
  views[5] = "設定頁面";
  views[2] = "首頁";
  views[9] = "我的頁面";
  views[1] = "啟動畫面";

  print("\n有序 TreeMap:");
  for (var e in views.entries) {
    print("${e.key} : ${e.value}");
  }
}Code language: PHP (php)

泛型集合的執行期型別

型別實體化(reified)

Dart 的泛型是實體化(reified)的:執行期會完整保留泛型型別資訊。

你可以在執行期直接判斷集合的具體泛型型別:

void main() {
    var names = <String>[];
	names.addAll(['Seth', 'Kathy', 'Lars']);
	print(names is List<String>); // 輸出 true
}Code language: PHP (php)

補充對照:Java 的泛型採用型別抹除,執行期會刪除泛型參數。在 Java 中只能判斷物件是否為 List,無法區分 List<String>List<int>

* 限制泛型參數型別

邊界 bound,由於讀者尚未學習類別,此章節僅需簡單瞭解即可。

定義泛型時,可以限制傳入的型別必須是某個父類別的子類別,這種約束稱為型別邊界,透過 extends 關鍵字實作。

1 限制不可空型別

強制泛型只能接受不可空型別(預設上界為可空的 Object?):

class Foo<T extends Object> {
  // 傳入的 T 不能是可空型別
}
Code language: JavaScript (javascript)

2 限定為指定父類別的子類別

使用 extends 指定基底類別,如此就能直接呼叫基底類別所有成員方法:

class Foo<T extends SomeBaseClass> {
  String toString() => "實例 Foo<$T>";
}

class Extender extends SomeBaseClass {}
Code language: JavaScript (javascript)

合法傳入型別:父類別本身、任意子類別

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();
Code language: HTML, XML (xml)

不傳泛型參數時,預設使用邊界型別:

var foo = Foo();
print(foo); // 輸出:實例 Foo<SomeBaseClass>
Code language: PHP (php)

傳入無關型別會直接觸發靜態編譯錯誤:

var foo = Foo<Object>(); // 編譯錯誤:Object 不是 SomeBaseClass 的子類別
Code language: JavaScript (javascript)

* 自參考型別邊界(F-bound 有界泛型)

約束泛型時,可以讓邊界參考自身的型別參數,形成自我約束,稱為 F-bound。

標準範例:可比較介面 Comparable<T>

abstract interface class Comparable<T> {
  int compareTo(T o);
}

// T 必須實作 Comparable<T>,只能和同型別物件比較
int compareAndOffset<T extends Comparable<T>>(T t1, T t2) =>
    t1.compareTo(t2) + 1;

class A implements Comparable<A> {
  @override
  int compareTo(A other) {
    // 實作比較邏輯
    return 0;
  }
}

int res = compareAndOffset(A(), A());
Code language: PHP (php)

T extends Comparable<T> 這個約束的含義:T 僅能與自身同型別的實例進行比較。

* 泛型方法

一般函式、類別方法同樣支援泛型型別參數:

T first<T>(List<T> ts) {
  // 前置檢查邏輯
  T tmp = ts[0];
  // 後續處理邏輯
  return tmp;
}
Code language: PHP (php)

完整範例

T first<T>(List<T> ts) {
  if (ts.isEmpty) {
    throw ArgumentError("清單不可為空");
  }
  T tmp = ts[0];
  return tmp;
}

void main() {
  List<String> strList = ["apple", "banana", "orange"];
  String firstStr = first(strList);
  print(firstStr); //apple

  List<int> numList = [10, 20, 30];
  int firstNum = first(numList);
  print(firstNum);//10
}Code language: Dart (dart)

泛型參數 <T> 作用於三處:

  1. 函式回傳值型別 T
  2. 輸入參數型別 List<T>
  3. 區域變數型別 T tmp

Previous:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *