Rust:臨時變量的生命週期

Rust 中臨時變量的生命週期是一個複雜但經常被忽略的話題。在通常情況下,Rust 將臨時變量保留足夠長的時間,這樣我們就不必考慮它們的生命週期了。然而,在很多情況下,這並不能滿足我的需求。

在這篇文章中,我們重新瞭解臨時變量的生命週期規則,分析一些延長臨時變量生命週期的用例。

臨時變量

下面是一個沒有上下文的 Rust 語句,它使用了一個臨時字符串變量:

f(&String::from('🦀'));

這個臨時字符串存在多長時間?如果我們設計 Rust 語言,我們基本上可以從兩種選項中選擇:

如果我們使用選項 1,上面的語句將總是導致借用檢查錯誤,因爲我們不能讓 f 借用已經不存在的東西。

因此,Rust 選擇了選項 2:首先分配 String,然後將對它的引用傳遞給 f,只有在 f 調用返回後才刪除臨時 String。

在 let 語句中

現在有一個稍微難一點的問題:

let a = f(&String::from('🦀'));
…
g(&a);

再問一次:臨時字符串變量的生命週期是多長?

這一次,選項 1 可能有效,取決於 f 的簽名。如果 f 被定義爲 fn f(s: &str) -> usize,那麼在 let 語句之後立即刪除 String 是完全可以的。

然而,如果 f 被定義爲 fn f(s: &str) -> &[u8],那麼 a 將從臨時 String 變量中產生借用,因此如果我們將 a 保留更長時間,我們將得到一個借用檢查錯誤。

對於選項 2,它在兩種情況下都可以很好地編譯,但是我們可能會保留一個臨時變量比必要的存活時間更長,這可能會浪費資源或導致微妙的錯誤 (例如,當 MutexGuard 比預期的更晚被丟棄時,會出現死鎖)。

在選項 1 和選項 2 之間,Rust 選擇了選項 1:在 let 語句末尾刪除臨時變量。手動將 String 移動到單獨的 let 語句中以使其保持更長的生命週期。

let s = String::from('🦀');
let a = f(&s);
…
g(&a);

在嵌套調用中

再看一個更復雜的:

g(f(&String::from('🦀')));

同樣,有兩種選擇:

該代碼段與前一個代碼段幾乎相同:將對臨時 String 變量的引用傳遞給 f,並將其返回值傳遞給 g。不過,這一次,使用了單個的嵌套調用表達式語句。

根據 f 的簽名,選項 1 可能起作用,也可能不起作用,選項 2 可能使臨時變量的生命週期存在的時間比必要的長。

選項 1 會使像 String::from('🦀').as_bytes().contains(&0x80) 這樣簡單的東西也不會通過編譯,因爲字符串會被丟棄在 as_bytes[f] 之後,在 contains[g] 之前。

因此,Rust 選擇了選項 2:不管 f 的簽名是什麼,String 都保持存活,直到語句結束,直到調用 g 之後。

在 if 語句中

現在讓我們來看一個簡單的 if 語句:

if f(&String::from('🦀')) {
    …
}

同樣的問題:什麼時候刪除臨時字符串變量?

在這種情況下,沒有理由在 if 語句體期間保持臨時變量的存活。該條件的結果是一個布爾值 (只有 true 或 false),根據定義,它不借用任何東西。

所以,Rust 選擇了選項 1。

一個有用的例子是使用 Mutex::lock,它返回一個臨時的 MutexGuard,當它被丟棄時將解鎖互斥鎖:

fn example(m: &Mutex<String>) {
    if m.lock().unwrap().is_empty() {
        println!("the string is empty!");
    }
}

這裏,m.lock().unwrap() 中的臨時變量 MutexGuard 在.is_empty() 之後被刪除,這樣在 println 語句期間互斥量就不會不必要地保持鎖定狀態。

在 if let 語句中

但是,if let 和 match 的情況不同,因爲我們的表達式不一定求值爲布爾值:

if let … = f(&String::from('🦀')) {
    …
}

還是有兩種選擇:

這一次,我們有理由選擇第二種而不是第一種。在 if let 語句或 match 這種模式匹配語句中,借用某些東西是很常見的。

因此,在這種情況下,Rust 選擇了選項 2。例如,如果我們有一個 Mutex<Vec> 類型的 vec,下面的代碼編譯得很好:

if let Some(x) = vec.lock().unwrap().first() {
    // 互斥對象仍然被鎖在這裏
    // 因爲我們從Vec中借用了x. (`x` 是 `&T`)
    println!("first item in vec: {x}");
}

我們從.lock().unwrap() 中獲得一個臨時變量 MutexGuard,並使用. first() 方法借用第一個元素。這個借用在 if let 的整個主體中需要持續鎖定,因此 MutexGuard 只在最後的} 處被刪除。

然而,在某些情況下,這並不是我們想要的。例如,如果我們不使用 first,而是使用 pop,它返回一個值而不是引用:

if let Some(x) = vec.lock().unwrap().pop() {
    // 互斥對象仍然被鎖在這裏
    // 這是不必要的,因爲我們沒有從Vec中借用任何東西。(“x” 是 “T”)
    println!("popped item from the vec: {x}");
}

這可能會導致細微的錯誤或性能降低。

目前,解決方法是使用單獨的 let 語句,將臨時生命週期限制爲 let 語句中:

let x = vec.lock().unwrap().pop(); // MutexGuard在此語句之後被刪除
if let Some(x) = x {
    …
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1zMRimkim1MX5EVZR0EI_g