深入 Rust

特徵與特徵界限(trait and trait bound)

假設你想寫一個函式,能印出任何「可列印的東西」,或排序任何「可比較的東西」,而不在乎它是數字、字串、還是你自訂的結構。你需要一種方式來說「這個型別支援這些操作」,作為一份許多不同型別都能滿足的契約。特徵(trait)就是 Rust 對那份契約的稱呼:一組具名的方法簽章(有時還有關聯型別或常數),一個型別可以宣告自己實作了它。這和 Java 的介面或 C++ 的 concept 是同一個想法——關於一個型別「能做什麼」的承諾,與它「怎麼做」分離。

你用一個特徵所要求的方法來定義它,例如 trait Summary { fn summarize(&self) -> String; }。型別接著用一個 impl 區塊來實作它:impl Summary for Article { fn summarize(&self) -> String { ... } }。現在 Article 滿足了 Summary 契約。特徵界限(trait bound)則是你在泛型程式碼中「使用」那份契約的方式:它把一個型別參數約束到只有實作了給定特徵的那些型別。寫作 fn notify<T: Summary>(item: &T),讀作「對於任何實作了 Summary 的型別 T,這是 notify」;在函式本體裡你可以呼叫 item.summarize(),因為界限保證了該方法存在。當約束變長時,你把它們移進一個 where 子句,where T: Summary + Clone, Wrapped: Display,更可讀地說同一件事。界限在編譯期檢查:用一個沒實作 Summary 的型別去呼叫 notify,程式就無法編譯,並精確指出缺了哪個特徵。

為何重要:特徵加上特徵界限,正是 Rust 不靠繼承來做多型的方式。它們讓你寫一個泛型函式,對每個滿足契約的型別都能運作,且滿足與否在編譯期被證明、執行期零檢查。誠實的微妙之處在於「定義」(impl Trait for Type,宣告某型別履行契約)與「約束」(T: Trait,向一個型別參數要求契約)之間的分別——它們用相關的語法卻做相反的工作。特徵撐起這個領域幾乎其餘的一切:分派、泛型、Send/Sync、Drop 與閉包,全都是特徵。

trait Summary { // 契約 fn summarize(&self) -> String; } struct Article { title: String } impl Summary for Article { // Article 履行它 fn summarize(&self) -> String { format!("Article: {}", self.title) } } // 特徵「界限」:T 必須實作 Summary。 fn notify<T: Summary>(item: &T) { println!("News! {}", item.summarize()); } // notify(&5); // 錯誤:i32 沒有實作 Summary

特徵命名一份契約;impl 為某型別履行它;特徵界限向泛型參數要求它——全在編譯期檢查。

「定義」與「約束」長得像卻不同:impl Trait for Type 說某型別滿足契約;T: Trait 向參數要求契約。把這兩者搞混,是初學特徵時最常見的錯誤。

又稱
traittrait boundwhere clauseinterface特徵界限where 子句