若你查閱基礎陣列型別 List 的 API 文件,會發現它真實的型別其實是 List<E>。<...> 這種尖括號標記代表這是一個泛型型別(參數化型別),也就是帶有形式型別參數的型別。依照慣例,絕大多數型別變數都使用單一字母命名,例如 E、T、S、K、V
為什麼要使用泛型?
- 泛型最核心的作用是保障型別安全
- 正確指定泛型可以產生更優化的執行期程式碼;
- 使用泛型能大幅減少程式碼重複。
如果你希望一個清單只能存放字串,就可以宣告為 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> 作用於三處:
- 函式回傳值型別
T - 輸入參數型別
List<T> - 區域變數型別
T tmp