深入 Rust

變異性(變異性 / 反變 / 不變)

來個謎題。如果一個活在長區域 'long 的參考是有效的,你能不能在預期一個活在較短區域 'short 之參考的地方使用它?直覺上可以——活得比一個短窗口久的東西,當然撐得過那個窗口。所以一個長壽的借用可以頂替一個短壽的借用。Rust 用生命週期之間的子型別關係來建模這件事:當 'long 活得比 'short 久時,'long 是 'short 的子型別。變異性就是一組規則,用來決定:當一個生命週期坐在一個更大的型別之內(在 &T、Vec<T>、fn(T)、Cell<T> 等等之內)時,這個子型別關係能否貫穿過去、又是往哪個方向。

結果有三種,而你其實只需略知一二。共變(covariance)是自然的同向情況:若 'long 是 'short 的子型別,那麼 &'long T 可用在想要 &'short T 的地方。多數唯讀容器對其生命週期與型別都是共變的,這正是為什麼你能把一個較長壽的參考傳給一個只要求較短壽參考的函式。逆變(contravariance)是反向的情況,且只出現在函式引數上:一個接受「較短壽」參考的函式更為通用,所以 fn(&'short T) 可用在預期 fn(&'long T) 的地方——這個翻轉之所以發生,是因為你「放進」函式的東西,流向與你「拿出來」的東西相反。不變(invariance)意指兩個方向都不允許替換:生命週期必須完全相符。任何讓你既能讀又能寫內層型別的東西都會逼出不變性,例如 &mut T 或 Cell<T>——因為若能替換,你就能在一個生命週期讀、在另一個生命週期寫,把一個短壽參考偷渡進長壽的位置,破壞健全性。

為何重要:變異性幾乎完全隱形——編譯器會從每個型別的構造方式自動算出它,而你從不寫出這個詞。你只在某個錯誤抱怨兩個生命週期必須完全相等、而你原以為其中一個能彈性順應另一個時才會遇上它;元兇通常是某個 &mut 或某個內部可變性型別逼出了不變性。誠實的重點是觀念上的:唯讀位置是共變的(較長壽可替換),函式引數位置是逆變的(翻轉),任何可變的東西都是不變的(不可替換)。你很少親手操控它,但知道它存在,能解釋那些原本令人費解的生命週期錯誤。

// 共變:較長壽的 &str 可放進想要較短壽者的地方。 fn use_short(s: &str) { println!("{s}"); } let literal: &'static str = "forever"; // 活整個程式 use_short(literal); // OK:&'static 可替換任何 &'a // 不變:&mut T 把生命週期釘死;兩邊都不能彈。 // fn(x: &mut &'short i32) 不能接受 &mut &'long i32 —— 健全性禁止它。

唯讀位置可彈(共變);可變位置剛硬(不變)——而正是這份剛硬,讓型別系統保持健全。

你從不宣告變異性;編譯器會推導它。你只在某個生命週期錯誤要求完全相等時才感受到它——幾乎總是因為某個 &mut T 或內部可變性型別讓那個位置變成了不變。

又稱
covariancecontravarianceinvariancesubtyping variance共變逆變型別變異