ETH官方钱包

前往
大廳
主題 達(dá)人專欄

搞懂 Rust 的所有權(quán)機(jī)制(上)

解凍豬腳 | 2024-08-30 19:00:08 | 巴幣 336 | 人氣 757

 
本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版



有了前面關(guān)於字串、可變性、記憶體操作的前置知識,終於可以來講 Rust 的核心設(shè)計:所有權(quán)。


? 初步瞭解所有權(quán)

之前我們提到 C++ 使用 RAII 的機(jī)制來管理資源釋放問題,資源本身可以定義建構(gòu)和解構(gòu)函式,讓系統(tǒng)在變數(shù)離開作用域時自動呼叫其值定義的解構(gòu)函式,讓工程師不必手動釋放資源。Rust 在記憶體管理的策略也是偏向 RAII,並且做得更嚴(yán)格。它除了把 RAII 做得更極致以外,還強(qiáng)調(diào)變數(shù)和值之間的擁有關(guān)係,藉此決定這些值的生命週期、何時該被釋放。

這說起來非常抽象,來看看具體的例子吧。我們先定義 Person 結(jié)構(gòu)體,接著宣告一個 Person 並且賦值給 p1:
#[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 變數(shù)「擁有」這個 Person 的值。若我們接下來使用另一個變數(shù) p2,令其為 p1 的值,然後再試圖把 p1 印出:
let p1 = Person {
    name: String::from("Kyaru"),
    height: 152.0,
    weight: 39.0,
}
let p2 = p1;
println!("{p1:?}");

你會馬上收到一個編譯錯誤:borrow of moved value: `p1`。

實際上,我們在執(zhí)行 p2 = p1 的時候,它真正的意思是「把 p1 擁有的值轉(zhuǎn)讓給 p2」,這在 Rust 當(dāng)中被稱為 move。實際上,Person 的值沒有發(fā)生任何複製操作,它只是語義上被「移動」給了 p2。當(dāng)它被移動給 p2 以後,變數(shù) p1 不再擁有任何值,你也就不能對 p1 做任何事了。

若你希望 p1 和 p2 各自擁有一個同樣值的 Person,就必須給 Person 加上 Clone 特徵,呼叫 .clone() 方法複製出一個新的 Person 以後再交給 p2:
#[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:?}");
}

你會發(fā)現(xiàn),無論是 p1 或 p2 的內(nèi)容都能被印出來了。這裡的 clone() 其實就是把底下的 name、height、weight 各自複製一份,建成一個新的 Person。現(xiàn)在,p1 和 p2 各自擁有的 Person 是相互獨(dú)立的個體,也就是說 p1 的 Person 被修改時,p2 的 Person 不會受到影響,反之亦然:
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 }

這個例子簡單易懂,但單單是這樣還不太能體現(xiàn)所有權(quán)和資源釋放之間的關(guān)聯(lián)。現(xiàn)在我們試著寫個函數(shù),把字串傳進(jìn)去:
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 的內(nèi)容印出來,就又會遇到同樣的問題:borrow of move value `name`。

沒有錯,這是因為在上面例子當(dāng)中,呼叫函數(shù)的時候也發(fā)生了所有權(quán)的轉(zhuǎn)移:當(dāng)我們呼叫 say_hello 並且把 name 傳進(jìn)去的時候,這份字串的所有權(quán)也被轉(zhuǎn)給函數(shù)的參數(shù) name_to_display 了。因為字串的所有權(quán)已經(jīng)轉(zhuǎn)移到 say_hello 函數(shù)當(dāng)中,所以根據(jù) Rust 的機(jī)制,字串的生命週期會在 say_hello 函數(shù)執(zhí)行完的時候一起結(jié)束(被系統(tǒng)自動釋放),那麼該字串當(dāng)然就不再有效,也就不能再被後續(xù)的 println! 使用。



? Rust 學(xué)習(xí)者的第一個課題:所有權(quán)應(yīng)該被轉(zhuǎn)移嗎?

學(xué)習(xí) Rust 之後,為了讓程式變得更嚴(yán)謹(jǐn),你要思考的事情就不再那麼簡單了。

在上面的例子當(dāng)中,我們把字串傳給函數(shù)之後,就不能再直接從 main 函數(shù)使用它了──那假如我希望字串後續(xù)能再被其他函數(shù)使用呢?

Rust 當(dāng)然不可能沒有考慮到這點(diǎn)。為了讓開發(fā)者可以更精細(xì)地控制資源何時被釋放,你可以選擇轉(zhuǎn)移所有權(quán),也可以選擇只是「借用」:
fn say_hello(name_to_display: &String) {
    println!("Hello, {name_to_display}!");
}

fn main() {
    let name = String::from("Kyaru");
    say_hello(&name);
    println!("{name}");
}

當(dāng)我們使用 &String 的時候,表示的是一個對於字串的不可變引用,也就是說函數(shù) say_hello 會暫時從 name 那邊把字串借過來。

我們也可以獲得它的「可變引用」(前提是變數(shù)本身也必須是可變的):
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"); // 必須是可變變數(shù),才能獲得變數(shù)的可變引用
    upgrade(&mut name); // 將 name 以可變的形式借用給 upgrade 函數(shù)
    say_hello(&name); // 將 name 以不可變的形式借用給 say_hello 函數(shù)
}

無論傳入的是可變引用或不可變引用,都不會發(fā)生所有權(quán)的轉(zhuǎn)移,這個字串仍然屬於外頭的變數(shù) name,且它的作用域、生命週期仍然在整個 main() 範(fàn)圍。

這裡要補(bǔ)充一點(diǎn):雖說 String 的不可變引用是 &String,但依照慣例用 &str 取代 &String 的寫法會更好,參數(shù)型態(tài)設(shè)為 &str 的話也就能接受 name.as_ref() 的寫法了。

當(dāng)然,所有權(quán)也可以傳進(jìn)去以後再傳出來(函數(shù)的參數(shù) name 前面必須加上 mut,才能修改):
fn with_type(mut name: String) -> String {
    if !name.ends_with(" (cat)") {
        name.push_str(" (cat)");
    }
    name // 當(dāng) return name; 在函數(shù)最後一行時,可以直接簡化為 name
}

fn main() {
    let mut name = String::from("Kyaru");
    upgrade(&mut name);

    let new_name = with_type(name);
    say_hello(&new_name);
}

在上面範(fàn)例中,name 被傳入 with_type 以後,字串的內(nèi)容被函數(shù)修改以後又被 return 回來,因此字串的所有權(quán)隨之被傳出來了。既然物件跟所有權(quán)被傳出來,我們自然就需要用變數(shù) new_name 接住它,而原本的變數(shù) name 因為不再擁有任何東西,自然就無效了:
let mut name = String::from("Kyaru");
upgrade(&mut name);
let new_name = with_type(name);
say_hello(&new_name);
println!("{name:?}"); // 這句不能被編譯,因為 name 擁有的字串最後交給 new_name 了,現(xiàn)在 name 什麼都沒有

或是你也可以選擇直接用原來的變數(shù) name 接住:
let mut name = String::from("Kyaru");
upgrade(&mut name);
name = with_type(name);
say_hello(&name);
println!("{name:?}"); // 這句可以被編譯

從這個例子就可以很清晰地看見字串是如何被借用、傳進(jìn)去、丟出來了。



HackMD 好讀版:https://hackmd.io/@upk1997/rust-ownership-1

縮圖素材原作者:Karen Rustad T?lva(CC0 1.0)
送禮物贊助創(chuàng)作者 !
0
留言

創(chuàng)作回應(yīng)

我婆
才華洋溢
2024-08-30 19:30:37
愛德莉雅.萊茵斯提爾
強(qiáng)大豬腳讚賞~ヾ(*′?`*)?
2024-08-30 20:08:41
露米諾斯 Lumynous
移動語意實際上還是會有複製,或者說堆疊上的資料,在不考慮編譯器最佳化的情況下,一定會複製,畢竟資料沒辦法真的被「移動」過去,不過其成本相較之下小很多;或者說此處的避免複製指的是各種資源(不一定可複製),包含動態(tài)分配的記憶體、開啟的檔案等等。語意上移動強(qiáng)調(diào)的更多(?)是所有權(quán),p2 = p1 後,p1 成員 name 所指向之字串物件的所有權(quán)就轉(zhuǎn)移了,Rust 透過直接限制不能使用移動的變數(shù)來保證行為正確。另外,p2 = p1 的例子少了一個分號。
2024-08-31 12:32:50
追蹤 創(chuàng)作集

作者相關(guān)創(chuàng)作

更多創(chuàng)作