Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

variance

本集目標

理解 covariance、invariance 與 contravariance,知道型別包在參考、Cell 或函數裡之後,原本的 subtype 關係會怎麼變化。

本集是第 5 章生命週期與本附錄前兩集的補充。

概念說明

前面我們已經知道:若 'long: 'short,一個 &'long T 可以縮短成 &'short T。這表示生命週期之間存在一種 subtype(子型別)關係:保證較長的參考,可以用在只要求較短保證的地方。

但把型別放進其他型別後,這個關係不一定照原樣保留。研究「包裝前後的 subtype 關係如何變化」,就是 variance。

covariance:方向保持不變

先看共享參考:

fn use_short<'short>(value: &'short str, _marker: &'short ()) {
    println!("{value}");
}

fn main() {
    let forever: &'static str = "我活到程式結束";

    {
        let marker = ();
        use_short(forever, &marker);
    }
}

&'static str 的生命週期很長,但 use_short 只要求和 marker 一樣短。Rust 可以把長參考當成短參考使用。

我們說 &'a T'acovariant(協變)的:如果 'long'short 的 subtype,&'long T 也會是 &'short T 的 subtype,方向沒有改變。

Box<T>Vec<T>Option<T> 等擁有值的常見型別,對 T 通常也是 covariant。例如,若 'long: 'shortVec<&'long str> 就能用在需要 Vec<&'short str> 的地方。這種用法會把整個 Vec 的所有權一起交出去,原本的位置不能再用較長的生命週期型別存取同一個 Vec

invariance:內層型別不能跟著轉換

可變參考比較特別。&'a mut T 對生命週期 'a 仍然可以縮短,但它對裡面的 Tinvariant(不變)的。

原因是 &mut T 不只讓你讀 T,還讓你放進一個新的 T

假設我們想執行下面這個賦值:

*slot = short;

short 的型別是 &'short str。要讓賦值成立,左邊的 *slot 也必須是 &'short str。而 slot 解參考後會得到 *slot,所以 slot 的型別必須是:

&mut &'short str

因此,負責放入短期參考的函數會寫成:

fn replace_with_short<'short>(
    slot: &mut &'short str,
    short: &'short str,
) {
    *slot = short;
}

fn main() {}

函數裡的賦值沒有問題,因為 *slotshort 都是 &'short str。問題會出現在呼叫端:

fn replace_with_short<'short>(
    slot: &mut &'short str,
    short: &'short str,
) {
    *slot = short;
}

fn main() {
    let mut forever: &'static str = "原本的長期資料";

    {
        let temporary = String::from("短期資料");
        replace_with_short(&mut forever, &temporary);
    }

    println!("{forever}");
}

forever 的型別是 &'static str,所以 &mut forever 的型別是 &mut &'static str。但 temporary 只能提供 &'short str,因此 replace_with_short 的第一個參數需要 &mut &'short str

若允許呼叫,編譯器就得進行這項轉換:

&mut &'static str → &mut &'short str

接著函數裡的 *slot = short 便會把短期參考寫進 forever。可變借用結束後,forever 的型別仍宣稱自己是 &'static str,實際卻可能已經懸垂。因此 Rust 禁止的是呼叫端的轉換,不是函數裡那個兩邊型別相同的賦值。

因此,即使 &'static str 可以當成較短的 &str&mut &'static str 也不能跟著任意轉成 &mut &'short str。包進 &mut 後,內層 T 的 subtype 關係被鎖住了。

Cell<T>RefCell<T>Mutex<T> 這類提供 interior mutability 的型別,對 T 一律 invariant。雖然表面拿到的可能只是 &Cell<T>,它們仍能更換裡面的值。

contravariance:方向反轉

函數參數會出現第三種情況:contravariance(逆變)。

為了只觀察 variance,下面由外層函數先固定 'short'long

fn use_as_long_only<'short, 'long: 'short>(
    can_accept_short: fn(&'short str),
) -> fn(&'long str) {
    // fn(&'short str) 在這裡轉成 fn(&'long str)。
    can_accept_short
}

fn main() {}

'long: 'short&'long str 可以當成 &'short str 使用。但包進函數參數後,可以轉換的方向會反過來:

&'long str      → &'short str
fn(&'short str) → fn(&'long str)

use_as_long_only 傳回的函數只會收到 &'long str。原本的 can_accept_short 只要參考能活到 'short 就能處理;活得更久的 &'long str 當然也符合要求,所以這項轉換是安全的。

函數輸入型別的關係因此和直覺方向相反:要求較少、能接受更廣輸入的函數,可以放到要求較窄輸入的地方。函數參數對其型別是 contravariant。

相對地,函數回傳值和一般讀取相同,是 covariant:能回傳保證較長的參考,也就能滿足只要求較短參考的呼叫者。

一張速查表

型別位置對參數的 variance直覺
&'a T'aT covariant只能共享讀取,可以縮短保證
&'a mut T'a covariant,對 T invariant能更換 T,不能改寫內層承諾
*const TT covariant只能透過指標讀取 T,方向保持不變
*mut TT invariant能透過指標更換 T,不能改寫內層承諾
Box<T>Vec<T>Option<T>通常對 T covariant擁有值,不會從共享入口換入任意 T
Cell<T>RefCell<T>T invariant共享狀態下仍可能更換內容
fn(T) -> UT contravariant,對 U covariant能接受更廣的輸入,也能回傳保證更強的值

這張表用來查就好,不必背。遇到編譯器拒絕一個看似合理的生命週期縮短時,先問:「這個型別能不能把新的值寫進去?」如果可以,invariance 往往就是原因。

variance 不會改變執行結果

variance 完全是編譯時期的型別規則,不會在執行時期插入轉換,也不會真的改造參考。編譯器只是在判斷一個型別能不能安全地用在另一個型別的位置。

範例程式碼

fn choose_shorter<'short>(
    long: &'static str,
    short: &'short str,
    use_long: bool,
) -> &'short str {
    if use_long {
        long
    } else {
        short
    }
}

fn print_text(text: &str) {
    println!("函數收到:{text}");
}

fn use_as_long_only<'short, 'long: 'short>(
    can_accept_short: fn(&'short str),
) -> fn(&'long str) {
    // fn(&'short str) 在這裡轉成 fn(&'long str)。
    can_accept_short
}

fn main() {
    let local = String::from("區域字串");

    // 共享參考是 covariant:'static 可以縮短成 local 的生命週期。
    let selected = choose_shorter("長期資料", &local, true);
    println!("選到:{selected}");

    // 函數輸入是 contravariant:參數型別的轉換方向會反過來。
    let print_long: fn(&'static str) = use_as_long_only(print_text);
    print_long("來自字串字面值");
}

重點整理

  • variance 描述把型別放進另一個型別後,原本的 subtype 關係如何變化。
  • covariant 保留方向;共享參考、函數回傳值與許多擁有所有權的型別屬於這類。
  • invariant 不允許沿著 subtype 關係轉換;可寫入內層值的 &mut TCell<T> 等常見於這類。
  • contravariant 反轉方向;函數參數是主要例子。
  • variance 是編譯時期規則,不會延長生命週期或產生執行時期轉換。