# 搞懂 Rust 的所有權機制(上) 有了前面關於字串、可變性、記憶體操作的前置知識,終於可以來講 Rust 的核心設計:所有權。 <br/> ## ► 初步瞭解所有權 之前我們提到 C++ 使用 RAII 的機制來管理資源釋放問題,資源本身可以定義建構和解構函式,讓系統在變數離開作用域時自動呼叫其值定義的解構函式,讓工程師不必手動釋放資源。Rust 在記憶體管理的策略也是偏向 RAII,並且做得更嚴格。它除了把 RAII 做得更極致以外,還強調變數和值之間的擁有關係,藉此決定這些值的生命週期、何時該被釋放。 這說起來非常抽象,來看看具體的例子吧。我們先定義 `Person` 結構體,接著宣告一個 `Person` 並且賦值給 `p1`: ```rust #[derive(Debug)] // 給 Person 加上 Debug 特徵,使它能被 println! 印出 struct Person { name: String, height: f64, weight: f64, } fn main() { let p1 = Person { name: String::from("Kyaru"), height: 152.0, weight: 39.0, }; println!("{p1:?}"); } ``` 這個時候我們可以說 `p1` 變數「擁有」這個 `Person` 的值。若我們接下來使用另一個變數 `p2`,令其為 `p1` 的值,然後再試圖把 `p1` 印出: ```rust let p1 = Person { name: String::from("Kyaru"), height: 152.0, weight: 39.0, } let p2 = p1; println!("{p1:?}"); ``` 你會馬上收到一個編譯錯誤:borrow of moved value: `p1`。 實際上,我們在執行 `p2` = `p1` 的時候,它真正的意思是「把 `p1` 擁有的值轉讓給 `p2`」,這在 Rust 當中被稱為 `move`。實際上,`Person` 的值沒有發生任何複製操作,它只是語義上被「移動」給了 `p2`。當它被移動給 `p2` 以後,變數 `p1` 不再擁有任何值,你也就不能對 `p1` 做任何事了。 若你希望 `p1` 和 `p2` 各自擁有一個同樣值的 `Person`,就必須給 `Person` 加上 `Clone` 特徵,呼叫 `.clone()` 方法複製出一個新的 `Person` 以後再交給 `p2`: ```rust #[derive(Debug, Clone)] // Clone 特徵使得 Person 能夠被複製 struct Person { name: String, height: f64, weight: f64, } fn main() { let p1 = Person { name: String::from("Kyaru"), height: 152.0, weight: 39.0, }; let p2 = p1.clone(); println!("{p1:?}"); println!("{p2:?}"); } ``` 你會發現,無論是 `p1` 或 `p2` 的內容都能被印出來了。這裡的 `clone()` 其實就是把底下的 `name`、`height`、`weight` 各自複製一份,建成一個新的 `Person`。現在,`p1` 和 `p2` 各自擁有的 `Person` 是相互獨立的個體,也就是說 `p1` 的 `Person` 被修改時,`p2` 的 `Person` 不會受到影響,反之亦然: ```rust let mut p1 = Person { name: String::from("Kyaru"), height: 152.0, weight: 39.0, }; let mut p2 = p1.clone(); p1.weight = 50.5; p2.weight = 60.7; println!("{p1:?}"); // Person { name: "Kyaru", height: 152.0, weight: 50.5 } println!("{p2:?}"); // Person { name: "Kyaru", height: 152.0, weight: 60.7 } ``` 這個例子簡單易懂,但單單是這樣還不太能體現所有權和資源釋放之間的關聯。現在我們試著寫個函數,把字串傳進去: ```rust fn say_hello(name_to_display: String) { println!("Hello, {name_to_display}!"); } fn main() { let name = String::from("Kyaru"); say_hello(name); println!("{name}"); } ``` 若你想在呼叫完 `say_hello` 之後嘗試用 `println!` 把 `name` 的內容印出來,就又會遇到同樣的問題:borrow of move value `name`。 沒有錯,這是因為在上面例子當中,呼叫函數的時候也發生了所有權的轉移:當我們呼叫 `say_hello` 並且把 `name` 傳進去的時候,這份字串的所有權也被轉給函數的參數 `name_to_display` 了。因為字串的所有權已經轉移到 `say_hello` 函數當中,所以根據 Rust 的機制,字串的生命週期會在 `say_hello` 函數執行完的時候一起結束(被系統自動釋放),那麼該字串當然就不再有效,也就不能再被後續的 `println!` 使用。 <br/> ## ► Rust 學習者的第一個課題:所有權應該被轉移嗎? 學習 Rust 之後,為了讓程式變得更嚴謹,你要思考的事情就不再那麼簡單了。 在上面的例子當中,我們把字串傳給函數之後,就不能再直接從 `main` 函數使用它了──那假如我希望字串後續能再被其他函數使用呢? Rust 當然不可能沒有考慮到這點。為了讓開發者可以更精細地控制資源何時被釋放,你可以選擇轉移所有權,也可以選擇只是「借用」: ```rust fn say_hello(name_to_display: &String) { println!("Hello, {name_to_display}!"); } fn main() { let name = String::from("Kyaru"); say_hello(&name); println!("{name}"); } ``` 當我們使用 `&String` 的時候,表示的是一個對於字串的不可變引用,也就是說函數 `say_hello` 會暫時從 `name` 那邊把字串借過來。 我們也可以獲得它的「可變引用」(前提是變數本身也必須是可變的): ```rust fn say_hello(name_to_display: &String) { println!("Hello, {name_to_display}!"); } fn upgrade(person_name: &mut String) { // 傳入可變引用 if !person_name.ends_with("EX") { person_name.push_str("EX"); } } fn main() { let mut name = String::from("Kyaru"); // 必須是可變變數,才能獲得變數的可變引用 upgrade(&mut name); // 將 name 以可變的形式借用給 upgrade 函數 say_hello(&name); // 將 name 以不可變的形式借用給 say_hello 函數 } ``` 無論傳入的是可變引用或不可變引用,都不會發生所有權的轉移,這個字串仍然屬於外頭的變數 `name`,且它的作用域、生命週期仍然在整個 `main()` 範圍。 這裡要補充一點:雖說 `String` 的不可變引用是 `&String`,但依照慣例用 `&str` 取代 `&String` 的寫法會更好,參數型態設為 `&str` 的話也就能接受 `name.as_ref()` 的寫法了。 當然,所有權也可以傳進去以後再傳出來(函數的參數 name 前面必須加上 mut,才能修改): ```rust fn with_type(mut name: String) -> String { if !name.ends_with(" (cat)") { name.push_str(" (cat)"); } name // 當 return name; 在函數最後一行時,可以直接簡化為 name } fn main() { let mut name = String::from("Kyaru"); upgrade(&mut name); let new_name = with_type(name); say_hello(&new_name); } ``` 在上面範例中,`name` 被傳入 `with_type` 以後,字串的內容被函數修改以後又被 return 回來,因此字串的所有權隨之被傳出來了。既然物件跟所有權被傳出來,我們自然就需要用變數 `new_name` 接住它,而原本的變數 `name` 因為不再擁有任何東西,自然就無效了: ```rust let mut name = String::from("Kyaru"); upgrade(&mut name); let new_name = with_type(name); say_hello(&new_name); println!("{name:?}"); // 這句不能被編譯,因為 name 擁有的字串最後交給 new_name 了,現在 name 什麼都沒有 ``` 或是你也可以選擇直接用原來的變數 `name` 接住: ```rust let mut name = String::from("Kyaru"); upgrade(&mut name); name = with_type(name); say_hello(&name); println!("{name:?}"); // 這句可以被編譯 ``` 從這個例子就可以很清晰地看見字串是如何被借用、傳進去、丟出來。 <br/> ###### tags: `Rust` `程式設計`