深入 Rust

關聯型別(associated type)

當你寫一個特徵時,有時某個方法的簽章必須提到一個型別,而這個型別不是由特徵本身固定,而是由實作者來選。想想迭代器:每個迭代器都產出元素,但一個計數器產出整數、一個行讀取器產出字串、一個檔案走訪器產出路徑。特徵想說「呼叫 next 會回給你一個『我的』元素」,卻不想對「元素是什麼」做承諾。關聯型別就是寫在特徵內部的一個佔位型別,在那裡命名、由各個實作填入,好讓特徵能談論一個隨實作者而異的相關型別。

你在特徵內用 type 來宣告一個,像是 trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }。這裡 Item 是關聯型別;方法以 Self::Item 來指涉它。當你實作這個特徵時,你把 Item 釘死:impl Iterator for Counter { type Item = u32; ... },為那一個實作把元素型別固定成 u32。自然會問,為什麼不乾脆做成泛型參數 trait Iterator<Item>。差別在唯一性:用關聯型別時,一個給定型別恰好實作 Iterator 一次,它的 Item 也就確定了,所以編譯器永遠知道 next 回傳什麼,不用你寫出來。用泛型參數時,一個型別可以同時實作 Iterator<u32> 「和」 Iterator<String>,於是每個呼叫點都得說明是哪一個——更有彈性,但更吵雜。拇指法則是:當每個實作者只有一個自然選擇時用關聯型別(一個迭代器只有一種元素型別),而當一個型別該以好幾種方式實作這個特徵時用泛型參數。

為何重要:關聯型別正是迭代器程式碼讀起來如此乾淨的原因——你寫 x.next(),編譯器就從實作得知元素型別,不需任何標註。它們遍布標準函式庫:Iterator::Item、Deref::Target、Add::Output、許多特徵裡的錯誤型別。要記住的誠實分別是:關聯型別(每個實作一個,由實作者選)對比泛型特徵參數(可有多個實作,由呼叫者選);選錯邊會讓一個特徵要嘛太僵、要嘛用起來太吵。

struct Counter { n: u32 } impl Iterator for Counter { type Item = u32; // 這個實作者的元素型別 fn next(&mut self) -> Option<u32> { if self.n < 3 { self.n += 1; Some(self.n) } else { None } } } let total: u32 = Counter { n: 0 }.sum(); // 編譯器知道 Item = u32 println!("{total}"); // 6 (1 + 2 + 3)

Item 在特徵中命名、由各個實作者固定,所以編譯器永遠知道 next 回傳什麼。

當每個實作者有一個自然選擇時(一個迭代器產出一種元素型別)採用關聯型別;當一個型別該以好幾種不同方式實作這個特徵時,採用泛型特徵參數。

又稱
type member of a traitItemOutputtrait associated type特徵關聯型別