Rust:臨時變量的生命週期
Rust 中臨時變量的生命週期是一個複雜但經常被忽略的話題。在通常情況下,Rust 將臨時變量保留足夠長的時間,這樣我們就不必考慮它們的生命週期了。然而,在很多情況下,這並不能滿足我的需求。
在這篇文章中,我們重新瞭解臨時變量的生命週期規則,分析一些延長臨時變量生命週期的用例。
臨時變量
下面是一個沒有上下文的 Rust 語句,它使用了一個臨時字符串變量:
f(&String::from('🦀'));
這個臨時字符串存在多長時間?如果我們設計 Rust 語言,我們基本上可以從兩種選項中選擇:
-
在調用 f 之前,字符串會被立即刪除。
-
字符串只會在調用 f 之後被刪除。
如果我們使用選項 1,上面的語句將總是導致借用檢查錯誤,因爲我們不能讓 f 借用已經不存在的東西。
因此,Rust 選擇了選項 2:首先分配 String,然後將對它的引用傳遞給 f,只有在 f 調用返回後才刪除臨時 String。
在 let 語句中
現在有一個稍微難一點的問題:
let a = f(&String::from('🦀'));
…
g(&a);
再問一次:臨時字符串變量的生命週期是多長?
-
字符串在 let 語句的末尾被刪除,在 f 返回之後,但在 g 被調用之前。
-
在調用 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('🦀')));
同樣,有兩種選擇:
-
在調用 f 之後,但在調用 g 之前,字符串被刪除。
-
該字符串將在語句結束時刪除,因此在調用 g 之後。
該代碼段與前一個代碼段幾乎相同:將對臨時 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 語句的條件求值之後,但在 if 語句體執行之前 (即在 {處)。
-
在 if 函數體之後 (即在} 處)。
在這種情況下,沒有理由在 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 的主體之前 (即在{處) 刪除字符串。
-
在 if let 語句體之後 (即在} 處)刪除該字符串。
這一次,我們有理由選擇第二種而不是第一種。在 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