在<<The rust programming language>>的中譯版<<rust權威指南>>中,作者用了30頁的篇幅來闡述這個問題。
如作者所言,所有權是學習rust語言的基礎,不掌握這個,無需繼續往下,所以,這是初學rust就必須會的。
正是所有權概念和相關工具的引入,Rust才能夠在沒有垃圾回收機制的前提下保障記憶體安全。
一、變數的儲存方式和賦值方式
要進入rust所有權範圍討論問題,那麼必須先理解RUST的變數的儲存方式和賦值方式
rust出於各種目的,規定變數可以存放在棧和堆上:
- 棧-存放哪些編譯時期就知道大小的。通常儲存那些簡單的資料型別,例如整數、浮點、布林、字元、成員型別都是整數、浮點、布林、字元之一的元組
- 注意這是一個FILO(先進後出,或者是後進先出)型別的,好似堆碟子,反而最上面的最先用。
- 堆-存放那些編譯時期無法知道其大小的。例如String(字串)
棧變數轉賦
對於棧變數而言,把a賦值給b,僅僅是一種在棧中複製資料的過程-快速且成本小。
let a=10; let b=a;
上面這個程式碼中,就是把10複製一份給b。
堆變數轉賦
但是堆不同,這個就是所有權的問題的來源,見後文。
堆變數和java的類變數是相識的儲存方式,都是用一個地址指向實際的儲存區域,如下圖(來自書本):
所有權問題就是堆變數問題。
二、所有權規則是如何產生的?所有權規則是什麼?
2.1、規則怎麼來?
要理解所有權,就需要從rust的設計目的談起,否則難於理解有些概念。
按照rust官方的說法,rust要保證記憶體安全(一定會釋放掉),同時還需要保證高效(不能用java 那樣的gc管理器),同時還不能太繁瑣(像C++那樣手動釋放)
所以,他們想到了一個主意:一個變數應該用完就自動釋放(通常是立刻釋放)
透過設這種設定,那麼就解決了一些問題:
- 不會記憶體洩漏
- 不需要藉助額外的垃圾收集器,增大了用於系統程式設計的可行性
但這會引發一個問題,怎麼知道需要被移除的變數沒人用了?
像java那樣使用引用計數器之類的?但是現在不搞那一套,就需要定個規矩:一個變數一定要有所有者;而且任意時刻只能有一個所有者
和前面提到的結合起來,就是rust著名的所有權規則:
- 一個值(不是變數)一定要有所有者
- 而且任意時刻,一個值只能有一個所有者
- 值所有者離開作用域後,持有的值會被立刻釋放
因為任意時候一個值只有一個所有者,所以一旦所有者離開返回,就可以簡單“高效”地執行釋放操作,不用擔心還被誰用了。
規則就是為了達到這個目的。
這個規則有一個瑕疵:按照rust官方的說法,有個drop程式會立刻做這個事情,那麼這某種程度上會降低效能。
但是現代高階語言都沒有解決這個問題,哪怕C++之類的,所以這個就不算是一個問題了。大家都裝糊塗吧,也許某天cpu和作業系統變化了之後,可以解決這個問題。
所以,這應該是一個當前情況下,權衡後的可接受方案。
2.2、規則怎麼得到保證?
規則定了,那麼如何保證這些規則可以得到遵守?
1.明確作用域的
如果是有明確作用域的,例如{}或者函式,那麼很好理解:
fn test(){ let s:String=String::from("種瓜得豆"); println!("{}",s); }
絕大部分語言都是這麼規定,所以能夠立刻理解!離開函式或者明確的作用域,就應該要釋放掉。
2.在一個作用域之內
參考書中的經典例子:
let s1=String::from("錦繡中華"); let s2=s1; println!("{}",s1);
通不過編譯!
let s1=String::from("錦繡中華"); | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait 12 | let s2=s1; | -- value moved here 13 | println!("{}",s1); | ^^ value borrowed here after move
第一次接觸這個,我有一點震驚,是因為兩個沒有想到:
1.怎麼會不行?
2.編譯器就能夠解決這個問題,而不是在執行時發生。編譯器夠厲害的。
現在再回過頭看下這個編譯錯誤提示,提示大意是這樣的:
由於s1是String型別,所以發生的移動情況。String並沒有實現Copy trait(型別特性)(意思就是不能透過複製完成值的轉賦)
當指定s2=s1的時候,s1的值已經轉給了s2(s1已經沒有值了)
當再次使用s1的時候,這就是一種典型的錯誤行為:值被移動後企圖借用
透過這個提示我們可以看到:在一個作用域之內,如果一個變數轉賦給另外一個變數的時候,會發生一件事情:值會從一個變數轉到另外一個變數上
為什麼要這麼做? 因為只有這樣才能保證前面提到的第二個原則:任意時刻只能有一個變數擁有某個值
回頭再看編譯提示,我們還知道一點:類似s1這樣的變數只所以會發生這個值轉移情況,是因為s1是String,預設沒有實現Copy型別特性。
反過來說,如果String實現了Copy型別特性,那麼就不會發生這種編譯錯誤。
2.3、規則帶來的其它問題
雖然三個規則保證了目的,但是會帶來不少問題(麻煩),比較典型的問題就是:
一個變數的值被移走後,那麼再次使用原來的變數就變得麻煩了。 因為需要再次移動值。如果這樣寫程式碼,太冗餘囉嗦,誰也受不了!
以下是一個奇怪的例子。
fn main(){ let mut address:String=String::from("福建福州"); println!("{}",address); print_str(address); //再用address,那麼久會報錯,如果要不報錯,則必須 //插入諸如 address=xxx之類語句,把值移回來,或者重新賦值 println!("新地址:{}",address) ; //報錯 } fn print_str(s:String){ println!("{}",s); }
在上例中,如果為了避免println!("新地址:{}",address)報錯,必須需要在這句話之前做一些事情。這樣無疑太麻煩了。
如果按照上面這種方法,那麼rust就沒有存在的意義,因為一門語言不但要考慮效能、安全等,也需要考慮工程效果:不能讓工程師煩
為了解決這個問題,rust給出了三個解決方案
- 不可變引用-透過某種方式標記這是一種借用,值所有權沒有轉移。借用完成後會自動歸還。當前借用不能修改值
- 可變引用 -透過某種方式標記這是一種借用,值所有權沒有轉移,借用完成後會自動歸還。這個期間,其它變數不能同時用這個值。但是當前借用能修改這個值
- 切片引用-只借用了部分(也可以是全部),可以細分為不可變切片和可變切片
透過這3個方案,編譯器已經幫工程師默默地完成了借用和自動歸還的操作,工程師不需要再操心了。
借用是如何實現的,借用原書的2幅圖
圖_引用
圖_切片引用
原書還列出兩個引用原則:
- 在任何一段給定的時間內,你要麼只能擁有一個可變引用,要麼只能擁有任意數量的不可變引用
- 引用總是有效的
後文演示這幾個例子
2.3.1不可變引用
fn main() { let name = String::from("岳飛"); //引用,被引用多少次都無所謂 print_me(&name); print_me(&name); let n1 = &name; let n2 = &name; println!("n1={},n2={}", n1, n2); }
書寫格式: &s
name可以同時被不可變借用多次。
這意味著,併發情況下可以共用一個值。
2.3.2可變引用
書寫格式: &mut s
fn mut_borrow() { let mut s = String::from("飛翔"); let s1 = &mut s; println!("{}", s1); s1.push_str("在太空!"); println!("{}", s1); let s2 = &mut s; println!("{}", s2); //println!("{}",s1); //在已經借給s2的情況下,再使用s1會報錯 -- first borrow later used here let s3 = &mut s; s3.push_str("星光燦爛"); println!("{}", s3); let s4 = &mut s; let s5 = &mut s; let s6 = &s; }
由於只能同時有一個可變引用,所以併發是一個問題。
2.3.3切片引用
書寫格式:
特別地,字串切片型別寫成 &str。
同時定義多個不可變切片,並可同時用
fn show_string_slice(){ let name=String::from("ABCDEF GHIJKLMN"); let s1=&name[0..4]; let s2=&name[5..10]; println!("s1={},s2={}",s1,s2); }
其它切片演示:
可變的陣列切片
fn test_mut_slice(){ let mut numbers = [1, 2, 3, 4, 5]; let slice: &mut [i32] = &mut numbers[0..1]; // 獲取整個陣列的可變切片 slice[0] = 10; // 修改切片中的第一個元素,這也會修改原始陣列 println!("{:?}", numbers); // 輸出: [10, 2, 3, 4, 5] }
三、小節
- rust的值的所有權規則是一個非常獨特的內容,其它語言暫時沒有這樣的情況
- rust的所有權有一點難於理解,但務必理解,因為這是繼續的基礎,此關不通,不要考慮後面的學習內容
- rust的所有權,可以算是一種相對高效的主動垃圾回收機制,是一種可以接受的妥協
- rust提供了多種引用方式,使得同時共享一個值變得可能,例如引用,切片引用
- rust的編譯器默默地幹了很多的事情