第一次在 Swift 程式碼中遇到角括號 <T> 時,視線會停頓一下。函式名稱旁多了一個大寫字母,打開文件後,還會看到 func map<T>(_ transform: (Element) -> T) -> [T] 這類簽章。泛型是 Swift 中階的入門關卡。標準函式庫幾乎所有內容,包括 Array、Dictionary 和 Optional,都是用泛型打造的;跨過這道門檻後,才開始看得懂函式庫程式碼。
這是中階系列第 2 篇。我們會整理泛型解決的問題、何時需要限制條件(constraint)與 where 子句,以及效能表現。
泛型解決的問題 — 拒絕在重複與型別安全之間二選一
在沒有泛型的世界裡,試著建立「交換兩個值的函式」,問題很快就會浮現。
為 Int 建立的版本不能用於 String。依型別複製程式碼,又會增加重複。因此會想到「能接受任何型別的盒子」,在 Swift 中就是 Any。但一使用 Any,型別資訊就消失了。每次取出都要轉型,把 Int 放入、卻以 String 取出的錯誤可能通過編譯,最後在執行時崩潰。
總結來說,當時只有「沒有重複但危險的 Any」和「安全但依型別重複的複製」兩個選項。泛型拒絕這種二選一。
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
<T>是「稍後填入型別的空白欄位」宣告。呼叫時 T 會確定為具體型別,編譯器則會用該型別檢查全部內容。像 swapValues(&intA, &strB) 這種型別不相符的呼叫會產生編譯錯誤。程式碼只有一份,型別檢查卻依型別進行,這就是泛型的本質。
型別也完全相同。宣告 struct Stack<Element> 後,Stack<Int> 與 Stack<String> 會成為不同型別,編譯器也會阻止你把字串放進 Int 堆疊。Array 正是這種結構,而 Optional 如選用型別篇所見,也是泛型 enum enum Optional<Wrapped>。換句話說,你早就每天都在使用泛型。
限制條件 — 從「任何型別」到「具備這些能力的型別」
空白 T 的預設狀態是任何型別。但任何型別也代表什麼都不能做。編譯器不知道 T 的任何資訊,因此無法比較、輸出或相加。
func largest<T>(_ items: [T]) -> T? {
items.max(by: <) // 編譯錯誤 — T沒有保證可比較
}
這時限制條件登場。寫成 <T: Comparable>,就會附加「T 只能是採用 Comparable 的型別」這項條件,從此便能對 T 使用 <。
func largest<T: Comparable>(_ items: [T]) -> T? {
items.max()
}
限制條件不是損失,而是一種交換。縮小可接受型別的範圍,同時增加能對這些型別執行的操作。協定篇提到的「能力組合」概念,正是在這裡與泛型交會,因為限制條件使用的就是協定。
條件變複雜時,就用 where 子句表示。位置不同,意義相同;但要替型別參數的關聯型別加上條件時,where 就是必要的。
// Element為 Equatable的集合之間才能比較
func allEqual<C: Collection>(_ items: C) -> Bool where C.Element: Equatable {
guard let first = items.first else { return true }
return items.allSatisfy { $0 == first }
}
用來指向集合所容納元素型別的,就是關聯型別(associated type),例如 C.Element。這是下下篇才會深入討論的主題,現在只要記住 where 子句是設定該條件的位置即可。
補充一點實務經驗:限制條件只加上需要的部分即可。只需要 Comparable 的函式若連 Hashable 也要求,能使用的型別反而會變少。函式本體實際使用的能力,就是限制條件的精確清單。
效能 — 編譯器消除了抽象化的成本
面對「泛型不是很慢嗎?」這個問題,可以用 Swift 的代表性最佳化來回答:特化(specialization)。
原理上,泛型函式不知道會傳入哪種型別,因此必須在執行時攜帶型別資訊並間接運作。但只要編譯器看得到呼叫位置,就會另外產生專供 swapValues<Int> 的版本。產生的機器碼會和手寫 Int 版本相同。使用泛型抽象化不代表要付出執行時成本;編譯器會剝除抽象化,產生具體程式碼。這正是哲學篇第 1 篇所說的零成本抽象化代表案例。
在同一個模組內,這項最佳化通常運作良好;跨越模組邊界後則會受到限制(取決於函式庫的發佈形式)。實務結論是,日常程式碼不必因擔心效能而避開泛型,真正的瓶頸應透過測量找出。我們已在另一篇文章討論過過早最佳化。
這裡自然會出現一個問題:「如果用協定型別接收(像 items: [Comparable]),那和泛型有什麼不同?」問得好,這正是下一篇的主題。泛型是編譯時確定型別的靜態多型;協定型別(existential)則是執行時混合不同型別的動態多型。下一篇會接著說明 Swift 為何用 some 與 any 關鍵字明確區分兩者。
何時該建立泛型 — 實務判斷標準
閱讀泛型與親自設計泛型是不同層次,因此整理建立泛型時的判斷標準。
像 ** 的邏輯只改變型別,卻第二次出現時,就是訊號。** 一開始就想著「總有一天會有其他型別」而使用泛型,通常都是過度設計。遵循 YAGNI (You Aren’t Gonna Need It — 尚未需要就不要建立) 原則,先從具體型別開始,真的出現重複時再泛化。
**比起網域概念,更適合結構與演算法。**快取、分頁回應、堆疊等與內容無關的結構,就是泛型的主場。像 APIResponse<User>、Cache<ImageKey, UIImage> 一樣。反過來,硬要把訂單付款等特定網域邏輯泛型化,只會讓簽章變難理解。
**當簽章複雜度超過使用端的收益時,就是該退回的訊號。**型別參數一旦有三、四個,where 子句又有三行,就該問問自己呼叫它的同事是否還讀得懂。Progressive Disclosure 篇提到的原則在這裡同樣適用:複雜度應由宣告端吸收,不能滲漏到使用端。
總結
- 泛型是拒絕在無重複的重用性與型別安全之間二選一的機制。程式碼只有一份,型別檢查則依型別進行。
<T>是型別空白欄位的宣告,而限制條件(T: Comparable、where 子句)則以縮小空白範圍為代價,換取更多可執行的操作。- 多虧特化,泛型大多能達到與手寫具體程式碼相同的效能。
- 設計標準:第二次出現重複時泛化,將泛型用於結構與演算法,簽章複雜度超過收益時就停止。
下一篇是泛型的兄弟,也是近期 Swift 中最容易混淆的語法:some 與 any。我們還會深入探討 some View 的真面目與 existential 的成本。

![[Swift 中階 #2] 用泛型消除重複與風險 封面圖](/assets/images/posts/7d69dc2a-8c58-4422-956e-940173ebaf99/1.jpg)