系統程式語言Rust特點介紹(2)—— 所有權系統
- 2020 年 2 月 3 日
- 筆記
很抱歉,第2篇距離第1篇長達3個月。。。工作繁忙加上家裡事多。。。不找客觀原因了,咱們開始聊聊Rust的所有權系統。
Rust的所有權系統主要有3個特性組成:Ownership(所有權)、Borrowing(借用)和Lifetimes(生命周期)。其中ownership和borrowing基本上是聯合使用,實現了下面3個效果。
- 在沒有gc的條件下,保證了記憶體安全。(gc對於系統應用來說,是一個比較可怕的難題。因為你很難控制gc造成的性能抖動。)
- 每一個變數的值,有且只有一個owner。
- 當owner離開scope時,自動drop(這裡的drop可以理解為C++中的析構)。
熟悉C++的同學,看到這3個特性,應該可以想到這基本上就是unique_ptr的特性,而Rust對普通變數(非動態申請)也做了這樣的限制。通過這樣的限制,rust就就解決了常見的因並發競爭引發的記憶體問題。因為同一時刻,變數的owner只有一個,在編譯階段保證了不會有並發競爭的問題。
請看下面的程式碼:

在這段程式碼中s1為一個字元串變數,也就是「hello」字元串的owner。下面一行聲明s2 = s1,就發生了ownership的轉移。此時,「hello」字元串的owner不再是s1,而變成了s2。這樣,在後面println列印的時候,s1不再擁有值,就無法通過編譯。

這裡的編譯錯誤很明確,s1的值被move走了,也可以說被s2 borrow「借」走了。
細心的同學可能還發現除了紅色的錯誤外,還有一行藍色的提示String沒有實現Copy特性。在這裡可以略微解釋一下,對於沒有實現Copy特性的類型來說,賦值操作就進行了所有權轉移。而實現了Copy特性後,會發生真正的Copy動作。在Rust內置類型中,有的實現了Copy特性,有的沒有實現,按照Rust的說法,對於實現代價很小且常用的類型如整數,就擁有Copy特性,而String類型則沒有。同時,使用者可以為已有類型如這裡的String類型,增加Copy trait(特性),就避免了上面程式碼中的所有權轉移。
對於擁有Copy trait的類型來說,賦值操作執行的是Copy動作,而不是所有權的轉移。這樣的話,依然是一個值只有一個owner,並沒有違反Rust的設計原則。
再回到現在的主題,String沒有實現Copy trait,而我們現在又不會擴展內置類型,怎麼辦呢?我不想一賦值就轉移所有權怎麼辦?針對這種情況,可以顯示調用clone方法來實現。
接下來請看下面的程式碼。

前面幾行程式碼用來展示整數的賦值操作並不會發生所有權轉移。在大括弧中的程式碼,s2變數clone了s1的值,而不是borrow了所有權。在後面的程式碼中,s1又追加了新的字元串。看一下輸出結果。

x和y都可以順利列印,s1的值是「hello, world",而s2仍然是"hello"。這說明:1. 整數的賦值操作不會發生所有權轉移;2. s2 clone s1後,實際上是擁有了新的值,與s1完全分道揚鑣。
現在大家對Rust的ownership有一定了解了吧。接下來看一個例子,這是從其他語言切換到Rust後,基本上都會感到不適的示例。
// move example2 println!("*********************** example4 *****************************"); let mut s3 = String::from("hello"); show_str(s3); //s3 = show_str2(s3); //show_str3(&s3); println!("s3 is {}", s3); fn show_str(s:String) { println!("s is {}", s); }
在上面的程式碼中,定義了一個字元串變數s3,然後將其傳入到show_str函數中列印。這在一般的程式語言中,是非常常見的操作。然後在Rust中。。。

因為ownership的關係,導致編譯報錯。原因是在調用show_str的時候,s3的值的所有權被轉移給了show_str函數。因此在調用show_str之後,就不能合法的使用s3了。
解決這個問題的方案也很簡單,或者讓函數把owership傳遞迴來,或者不改變所有權。下面是兩種實現方案。
// move example2 println!("*********************** example4 *****************************"); let mut s3 = String::from("hello"); //show_str(s3); s3 = show_str2(s3); show_str3(&s3); println!("s3 is {}", s3); #[allow(dead_code)] fn show_str2(s:String) -> String { println!("s is {}", s); s } #[allow(dead_code)] fn show_str3(s: &String) { println!("s is {}", s); }
在上面的程式碼中,show_str2通過參數s獲得了String的ownership,但是又通過返回值將ownership歸還。而show_str3則將參數定義為引用類型,這樣在調用show_str3時,就不會發生ownership的轉移。
下面是上述程式碼的運行結果。

在show_str3中,我們見到了Rust中「引用」的語法。這時,我們要提出一個疑問,既然Rust支援引用,那豈不是又可以有多個變數擁有同一個值的所有權了?這樣豈不是違反了Rust的設計原則和安全限制,可能會導致對同一個值的並發修改?
答案自然是否定的。請看下面的示例程式碼:
// double mut referenc println!("*********************** example5 *****************************"); let r1 = &s3; let r2 = &s3; println!("r1 is {}, r2 is {}", r1, r2);
s3還是上面定義的可變String類型,這裡的r1和r2都是對s3的引用,準確的說是常量引用(記住Rust中變數類型默認都是常量的)。引用呢,實際上就是指向了s3值的記憶體。編譯運行,結果如下:

既沒有編譯錯誤,運行結果也如預期。為什麼呢?因為這樣的語法是安全的,這裡的r1和r2都是常量引用,只能讀取不能更改。自然這裡就沒有並發競爭的邏輯,因此Rust允許這樣的編碼邏輯。
如果我們在定義了常量引用之後,又企圖使用s3修改其值,會怎麼樣呢?
// double mut referenc println!("*********************** example5 *****************************"); let r1 = &s3; let r2 = &s3; s3.push_str("aaa"); //let r3 = &mut s3; println!("r1 is {}, r2 is {}", r1, r2);

錯誤提示,因為有r1和r2兩個常量引用,那麼就不能再修改s3了,即使之前s3被定義為mutable的變數。如果沒有嘗試修改s3的值,程式碼編譯運行就沒有問題,這說明了Rust的安全檢查不僅僅是簡單的語法限制,同時還做了上下文分析。
現在,我們再定義一個可變的引用類型r3來測試。
// double mut referenc println!("*********************** example5 *****************************"); let r1 = &s3; let r2 = &s3; let r3 = &mut s3; println!("r1 is {}, r2 is {}", r1, r2);
使用cargo build編譯。

出現編譯錯誤。提示r1進行了immutable borrow,就不能使用r3再進行mutable borrow。我們也可以這樣理解,已經有了只讀的引用,為了保證安全性,自然不能再進行可寫的引用指向了。
目前這裡的錯誤是因為前面有隻讀的引用,如果我們把r1和r2的程式碼注釋掉,邏輯是否就正確了呢?
// double mut referenc println!("*********************** example5 *****************************"); //let r1 = &s3; //let r2 = &s3; let r3 = &mut s3; //println!("r1 is {}, r2 is {}", r1, r2); println!("r3 is {}", r3);

編譯沒有錯誤,運行也正常。也許與某些同學的預期不符。因為r3既然是mut 類型的引用,豈不是有兩個變數(加上s3)可以同時更改s3的值了呢?
讓我們再加上s3的列印。
println!("*********************** example5 *****************************"); //let r1 = &s3; //let r2 = &s3; let r3 = &mut s3; //println!("r1 is {}, r2 is {}", r1, r2); println!("r3 is {}, s3 is {}", r3, s3);

符合預期出現編譯錯誤,再次驗證Rust的所有權系統,是不能允許有兩個變數有同時修改一個值的可能性。
至此,對於Rust的ownership,我想大家已經有了一定的認識了。Rust的所有權系統中的lifetime,只能等待另外一篇文章介紹了。另外,一些好奇的同學可能會想到,截止到目前為止,這些都是單執行緒程式。Rust如何在多執行緒,真正的並發編程下,保證的記憶體安全呢?我爭取很快再寫兩篇,一篇介紹lifetime,另外一篇介紹Rust的並發安全性。
PS:關於Rust系列文章的示例程式碼,大家可以在https://github.com/gfreewind/RustTraining上查看。