詳解Rust中的所有權(quán)機(jī)制
Rust中的所有權(quán)機(jī)制
什么是所有權(quán)
Rust 的核心功能(之一)是 所有權(quán)(ownership)。雖然該功能很容易解釋,但它對語言的其他部分有著深刻的影響。
所有程序都必須管理其運(yùn)行時使用計算機(jī)內(nèi)存的方式。
一些語言中具有垃圾回收機(jī)制,在程序運(yùn)行時有規(guī)律地尋找不再使用的內(nèi)存——例如Java、Python等;在另一些語言中,程序員必須親自分配和釋放內(nèi)存——例如C、C++等。
Rust 則選擇了第三種方式:通過所有權(quán)系統(tǒng)管理內(nèi)存,編譯器在編譯時會根據(jù)一系列的規(guī)則進(jìn)行檢查。如果違反了任何這些規(guī)則,程序都不能編譯。在運(yùn)行時,所有權(quán)系統(tǒng)的任何功能都不會減慢程序。
所以Rust具有安全性的原因之一是Rust的程序把大部分因?yàn)閮?nèi)存方面的安全問題在編譯時給予扼殺。
所有權(quán)規(guī)則
- Rust 中的每一個值都有一個 所有者(owner)。
- 值在任一時刻有且只有一個所有者。
- 當(dāng)所有者(變量)離開作用域,這個值將被丟棄。
String類型
其實(shí)這個已經(jīng)在數(shù)據(jù)類型那一節(jié)的時候就應(yīng)該簡單介紹一下它,但是也沒多大關(guān)系。String是一種結(jié)構(gòu)體,其原型如下:
pub struct String { vec: Vec<u8>, }
結(jié)構(gòu)體這個概念對于有C語言基礎(chǔ)的就不用多說了,前面還介紹過元組類型,元組其實(shí)就是更簡單一點(diǎn)的結(jié)構(gòu)體。
我們已經(jīng)見過字符串字面值,即被硬編碼進(jìn)程序里的字符串值。
字符串字面值是很方便的,不過它們并不適合使用文本的每一種場景。原因之一就是它們是不可變的。另一個原因是并非所有字符串的值都能在編寫代碼時就知道:例如,要是想獲取用戶輸入并存儲該怎么辦呢?
為此,Rust 有第二個字符串類型,String
。這個類型管理被分配到堆上的數(shù)據(jù),所以能夠存儲在編譯時未知大小的文本??梢允褂?from
函數(shù)基于字符串字面值來創(chuàng)建 String
,如下:
let s = String::from("hello");
這兩個冒號 ::
是運(yùn)算符,允許將特定的 from
函數(shù)置于 String
類型的命名空間(namespace)下,而不需要使用類似 string_from
這樣的名字。
關(guān)于String的一些簡單的操作:
往String類型的變量尾部插入字符和字符串分別可以使用push和push_str函數(shù)完成。
fn main() { let mut s = String::from("hello"); s.push(','); s.push_str(" world!"); println!("s = {}", s); }
結(jié)果:
s = hello, world!
因?yàn)镾tring對 ‘+’ 做了運(yùn)算符重載,所以上面的操作也可使用 '+'完成:
fn main() { let mut s = String::from("hello"); s = s + ","; //這里是雙引號 s = s + " world!"; println!("s = {}", s); }
String和&str的區(qū)別在于兩個類型對內(nèi)存的處理上。
內(nèi)存與分配
字符串字面值在編譯時就知道其內(nèi)容,所以文本被直接硬編碼進(jìn)最終的可執(zhí)行文件中。這使得字符串字面值快速且高效。不過這些特性都只得益于字符串字面值的不可變性。不幸的是,我們不能為了每一個在編譯時大小未知的文本而將一塊內(nèi)存放入二進(jìn)制文件中,并且它的大小還可能隨著程序運(yùn)行而改變。
對于 String
類型,為了支持一個可變,可增長的文本片段,需要在堆上分配一塊在編譯時未知大小的內(nèi)存來存放內(nèi)容。這意味著:
- 必須在運(yùn)行時向內(nèi)存分配器(memory allocator)請求內(nèi)存。
- 需要一個當(dāng)我們處理完
String
時將內(nèi)存返回給分配器的方法。
就是字符串字面量的內(nèi)存是在棧上,而String類型的內(nèi)存是在堆上。
在有 垃圾回收(garbage collector,GC)的語言中, GC 記錄并清除不再使用的內(nèi)存,而我們并不需要關(guān)心它。在大部分沒有 GC 的語言中,識別出不再使用的內(nèi)存并調(diào)用代碼顯式釋放就是我們的責(zé)任了,跟請求內(nèi)存的時候一樣。從歷史的角度上說正確處理內(nèi)存回收曾經(jīng)是一個困難的編程問題。如果忘記回收了會浪費(fèi)內(nèi)存。如果過早回收了,將會出現(xiàn)無效變量。如果重復(fù)回收,這也是個 bug。我們需要精確的為一個 allocate
配對一個 free
。
- 有GC的語言:不需要關(guān)心不再使用的內(nèi)存,因?yàn)闀詣覩C.
- 沒有GC的語言:一次申請內(nèi)存對應(yīng)一次釋放內(nèi)存。
Rust的處理方式:內(nèi)存在擁有它的變量離開作用域后就被自動釋放。
{ let s = String::from("hello"); // 從此處起,s 是有效的 // 使用 s } // 此作用域已結(jié)束, // s 不再有效
這是一個將 String
需要的內(nèi)存返回給分配器的很自然的位置:當(dāng) s
離開作用域的時候。當(dāng)變量離開作用域,Rust 為我們調(diào)用一個特殊的函數(shù)。這個函數(shù)叫做 drop
,在這里 String
的作者可以放置釋放內(nèi)存的代碼。Rust 在結(jié)尾的 }
處自動調(diào)用 drop
。
說白了Rust的機(jī)制就是對應(yīng)著C++的智能指針中unique_ptr的機(jī)制。
移動
在Rust 中,多個變量可以采取不同的方式與同一數(shù)據(jù)進(jìn)行交互。
例如:
- 標(biāo)量的版本
let x = 5; let y = x;
因?yàn)檎麛?shù)是有已知固定大小的簡單值,所以這兩個 5
被放入了棧中。
原因是像整型這樣的在編譯時已知大小的類型被整個存儲在棧上,所以拷貝其實(shí)際的值是快速的。這意味著沒有理由在創(chuàng)建變量
y
后使x
無效。換句話說,這里沒有深淺拷貝的區(qū)別,所以這里調(diào)用clone
并不會與通常的淺拷貝有什么不同,我們可以不用管它。
- String類型的版本
let s1 = String::from("hello"); let s2 = s1;
String
由三部分組成:
- 指向存放字符串內(nèi)容內(nèi)存的指針
- 長度
- 容量。
這一組數(shù)據(jù)存儲在棧上。String通過其內(nèi)部的成員指針,訪問到實(shí)際字符串的位置。如下圖所示:
在Rust中如果出現(xiàn)以下的情況時:
let s1 = String::from("hello"); let s2 = s1;
那么Rust采取的方式是,把s1的所有權(quán)移交給s2,那么此后s1不不可再對原來的內(nèi)存進(jìn)行操作(保證Rust中的所有權(quán)的規(guī)則)。
另外,這里還隱含了一個設(shè)計選擇:Rust 永遠(yuǎn)也不會自動創(chuàng)建數(shù)據(jù)的 “深拷貝”。因此,任何 自動 的復(fù)制可以被認(rèn)為對運(yùn)行時性能影響較小。
克隆
因?yàn)镽ust不會自動復(fù)制變量中的具體內(nèi)容,而有些場景中,我們有希望拷貝原來的內(nèi)容,那Rust也給我們提供了一種克隆的方式,這樣就符合我們原來的編碼習(xí)慣。
let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2);
這樣的話就沒有將s1的所有權(quán)移交給s2,而是s2拷貝了一份新的s1的內(nèi)容。
Rust 有一個叫做
Copy
trait 的特殊注解,可以用在類似整型這樣的存儲在棧上的類型上(第十章將會詳細(xì)講解 trait)。如果一個類型實(shí)現(xiàn)了Copy
trait,那么一個舊的變量在將其賦值給其他變量后仍然可用。
任何一組簡單標(biāo)量值的組合都可以實(shí)現(xiàn) Copy
,任何不需要分配內(nèi)存或某種形式資源的類型都可以實(shí)現(xiàn) Copy
。如下是一些 Copy
的類型:
- 所有整數(shù)類型,比如
u32
。 - 布爾類型,
bool
,它的值是true
和false
。 - 所有浮點(diǎn)數(shù)類型,比如
f64
。 - 字符類型,
char
。 - 元組,當(dāng)且僅當(dāng)其包含的類型也都實(shí)現(xiàn)
Copy
的時候。比如,(i32, i32)
實(shí)現(xiàn)了Copy
,但(i32, String)
就沒有。
所有權(quán)與函數(shù)
將值傳遞給函數(shù)與給變量賦值的原理相似。向函數(shù)傳遞值可能會移動或者復(fù)制,就像賦值語句一樣。
例如:
fn main() { let i = 9; fun(i); //程序走到這里時i依然有效 let s = String::from("hello"); fun2(s); //程序走到這里時s不再有效 } //在參數(shù)傳進(jìn)來的時候?qū)嶋H上是發(fā)生了 形參 = 實(shí)參 的事情 fn fun(x: i32) { ... } fn fun2(y: String) { ... }
上述例子就相當(dāng)于發(fā)生了以下的操作:
let i = 9; let x = i; //這里是發(fā)生了Copy,不會發(fā)生所有權(quán)的轉(zhuǎn)移 let s = String::from("hello"); let y = s; //這里因?yàn)镾tring類型是在堆上申請內(nèi)存,所以發(fā)生了所有權(quán)的轉(zhuǎn)移
如果想要使s調(diào)用完函數(shù)(移交所有權(quán)后),還能再次使用則需要把所有權(quán)移交回來,例如以下例子:
fn main() { let mut s = String::from("hello"); s = show_string(s); } fn show_string(x: String) -> String{ println!("{}", x); x }
引用與借用
因?yàn)橐平凰袡?quán)后再移交回來這種方式太笨了,所以Rust提供了一種引用的方式方便我們操作。
引用(reference)像一個指針,因?yàn)樗且粋€地址,我們可以由此訪問儲存于該地址的屬于其他變量的數(shù)據(jù)。 與指針不同,引用確保指向某個特定類型的有效值。
簡單來講就是引用就是有一種不好的感覺,我讓你幫我辦一件事,你辦事的工具從頭到尾都是你的,我從來就沒碰過,你只需要幫我把事情辦了就好。
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
注意我們傳遞 &s1
給 calculate_length
,同時在函數(shù)定義中,我們獲取 &String
而不是 String
。這些 & 符號就是 引用,它們允許你使用值但不獲取其所有權(quán)。
把上述calculate_length函數(shù)所表達(dá)的翻譯成“不好的”的方式表達(dá):
calculate_length時期,s是一個辦事不露面的人,他計劃著要辦len這件事,于是他讓s1幫他完成他愿望,因?yàn)閟1擅長處理len這件事。
變量 s
有效的作用域與函數(shù)參數(shù)的作用域一樣,不過當(dāng) s
停止使用時并不丟棄引用指向的數(shù)據(jù),因?yàn)?s
并沒有所有權(quán)。當(dāng)函數(shù)使用引用而不是實(shí)際值作為參數(shù),無需返回值來交還所有權(quán),因?yàn)榫筒辉鴵碛兴袡?quán)。
我們將創(chuàng)建一個引用的行為稱為 借用(borrowing)。正如現(xiàn)實(shí)生活中,如果一個人擁有某樣?xùn)|西,你可以從他那里借來。當(dāng)你使用完畢,必須還回去。我們并不擁有它。
可變引用
因?yàn)樵赗ust中,變量默認(rèn)都是不可變的,引用也是變量,所以當(dāng)我們需要修改內(nèi)存中的內(nèi)容的話需要加上mut關(guān)鍵字才可以進(jìn)行修改。
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
可變引用有一個很大的限制:如果你有一個對該變量的可變引用,你就不能再創(chuàng)建對該變量的引用。這些嘗試創(chuàng)建兩個 s
的可變引用的代碼會失?。?/p>
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:5:14 | 4 | let r1 = &mut s; | ------ first mutable borrow occurs here 5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 6 | 7 | println!("{}, {}", r1, r2); | -- first borrow later used here For more information about this error, try `rustc --explain E0499`. error: could not compile `ownership` due to previous error
這個報錯說這段代碼是無效的,因?yàn)槲覀儾荒茉谕粫r間多次將 s
作為可變變量借用。
這一限制以一種非常小心謹(jǐn)慎的方式允許可變性,防止同一時間對同一數(shù)據(jù)存在多個可變引用。新 Rustacean 們經(jīng)常難以適應(yīng)這一點(diǎn),因?yàn)榇蟛糠终Z言中變量任何時候都是可變的。這個限制的好處是 Rust 可以在編譯時就避免數(shù)據(jù)競爭。數(shù)據(jù)競爭(data race)類似于競態(tài)條件,它可由這三個行為造成:
- 兩個或更多指針同時訪問同一數(shù)據(jù)。
- 至少有一個指針被用來寫入數(shù)據(jù)。
- 沒有同步數(shù)據(jù)訪問的機(jī)制。
數(shù)據(jù)競爭會導(dǎo)致未定義行為,難以在運(yùn)行時追蹤,并且難以診斷和修復(fù);
Rust 避免了這種情況的發(fā)生,因?yàn)樗踔敛粫幾g存在數(shù)據(jù)競爭的代碼!
允許擁有多個可變引用,只是不能 同時 擁有:
let mut s = String::from("hello"); { let r1 = &mut s; } // r1 在這里離開了作用域,所以我們完全可以創(chuàng)建一個新的引用 let r2 = &mut s;
fn main() { let mut s = String::from("hello"); test(&mut s); let r2 = &mut s; println!("r2 = {}", r2); } fn test(x: &mut String) { x.push_str("world"); }
r2 = helloworld
Rust 在同時使用可變與不可變引用時也采用的類似的規(guī)則。
let mut s = String::from("hello"); let r1 = &s; // 沒問題 let r2 = &s; // 沒問題 let r3 = &mut s; // 大問題 println!("{}, {}, and {}", r1, r2, r3);
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:6:14 | 4 | let r1 = &s; // no problem | -- immutable borrow occurs here 5 | let r2 = &s; // no problem 6 | let r3 = &mut s; // BIG PROBLEM | ^^^^^^ mutable borrow occurs here 7 | 8 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `ownership` due to previous error
不能在擁有不可變引用的同時擁有可變引用。多個不可變引用是可以的,因?yàn)闆]有哪個只能讀取數(shù)據(jù)的人有能力影響其他人讀取到的數(shù)據(jù)。
錯誤版本:
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &mut s; //有問題,因?yàn)橄旅孢€在使用r1,r2 println!("{},{}", r1, r2); println!("{}", r3); }
正確版本:
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{},{}", r1, r2); // 此位置之后 r1 和 r2 不再使用 let r3 = &mut s; //沒問題,因?yàn)楹竺娌辉偈褂胷1,r2 println!("{}", r3); }
懸垂引用
在具有指針的語言中,很容易通過釋放內(nèi)存時保留指向它的指針而錯誤地生成一個 懸垂指針(dangling pointer),所謂懸垂指針是其指向的內(nèi)存可能已經(jīng)被分配給其它持有者。相比之下,在 Rust 中編譯器確保引用永遠(yuǎn)也不會變成懸垂?fàn)顟B(tài):當(dāng)你擁有一些數(shù)據(jù)的引用,編譯器確保數(shù)據(jù)不會在其引用之前離開作用域。
懸垂引用就是野指針的意思,有C/C++基礎(chǔ)的就不用多說了。
fn main() { let reference_to_nothing = dangle(); //訪問到了已經(jīng)被釋放內(nèi)存的地址,野指針 } fn dangle() -> &String { let s = String::from("hello"); &s //返回s所指向的地址 } //s離開其作用域,則s所指向的內(nèi)存被釋放
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0106]: missing lifetime specifier --> src/main.rs:5:16 | 5 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 5 | fn dangle() -> &'static String { | ~~~~~~~~ For more information about this error, try `rustc --explain E0106`. error: could not compile `ownership` due to previous error
像以下的代碼是沒有錯的:
fn no_dangle() -> String { let s = String::from("hello"); s }
因?yàn)榉祷氐牟皇且?,所以就相?dāng)于把s的所有權(quán)移交給一個String類型的無名對象,然后在函數(shù)調(diào)用那塊又把這個無名對象的所有權(quán)移交給接收者。
引用的規(guī)則
- 在任意給定時間,要么 只能有一個可變引用,要么 只能有多個不可變引用。
- 引用必須總是有效的。
Slice類型(切片)
slice 允許你引用集合中一段連續(xù)的元素序列,而不用引用整個集合。slice 是一類引用,所以它沒有所有權(quán)。
字符串切片
因?yàn)閟lice是一類部分引用,所以字符串切片就是原來字符串的一部分。
let s = String::from("hello world"); //0..5 左閉右開 [0,5) 左閉右閉 0..=5 [0,5] let hello = &s[0..5]; //hello let world = &s[6..11]; //world
- 在Range(范圍)中,如果左是0,則可以簡寫0
- 在Range(范圍)中,如果右是字符串長度(字符串尾部),則可以簡寫
- 如果以上兩個都滿足,則左和右都可以簡寫
let s = String::from("hello"); let slice = &s[0..2]; //he let slice = &s[..2]; //he let len = s.len(); let slice = &s[3..len]; //lo let slice = &s[3..]; //lo
注意:字符串 slice range 的索引必須位于有效的 UTF-8 字符邊界內(nèi),如果嘗試從一個多字節(jié)字符的中間位置創(chuàng)建字符串 slice,則程序?qū)蝈e誤而退出。出于介紹字符串 slice 的目的,本部分假設(shè)只使用 ASCII 字符集;第八章的 “使用字符串存儲 UTF-8 編碼的文本” 部分會更加全面的討論 UTF-8 處理問題。
字符串字面值被儲存在二進(jìn)制文件中。
let s = "Hello, world!";
這里 s
的類型是 &str
:它是一個指向二進(jìn)制程序特定位置的 slice。這也就是為什么字符串字面值是不可變的;&str
是一個不可變引用。
其他類型的 slice
以整型數(shù)組為例,當(dāng)然其他的類型也都是類似的。
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; //[2, 3] assert_eq!(slice, &[2, 3]);
總結(jié)
所有權(quán)、借用和 slice 這些概念讓 Rust 程序在編譯時確保內(nèi)存安全。
Rust 語言提供了跟其他系統(tǒng)編程語言相同的方式來控制你使用的內(nèi)存,但擁有數(shù)據(jù)所有者在離開作用域后自動清除其數(shù)據(jù)的功能意味著你無須額外編寫和調(diào)試相關(guān)的控制代碼。
到此這篇關(guān)于Rust中的所有權(quán)機(jī)制的文章就介紹到這了,更多相關(guān)Rust所有權(quán)機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Rust實(shí)現(xiàn)一個表達(dá)式Parser小結(jié)
這篇文章主要為大家介紹了Rust實(shí)現(xiàn)一個表達(dá)式Parser小結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Rust使用Sqlx連接Mysql的實(shí)現(xiàn)
數(shù)據(jù)庫在編程中是一個很重要的環(huán)節(jié),本文主要介紹了Rust使用Sqlx連接Mysql的實(shí)現(xiàn),記錄rust如何操作數(shù)據(jù)庫并以mysql為主的做簡單的使用說明,感興趣的可以了解一下2024-03-03Rust?Atomics?and?Locks內(nèi)存序Memory?Ordering詳解
這篇文章主要為大家介紹了Rust?Atomics?and?Locks內(nèi)存序Memory?Ordering詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Rust-使用dotenvy加載和使用環(huán)境變量的過程詳解
系統(tǒng)的開發(fā),測試和部署離不開環(huán)境變量,今天分享在Rust的系統(tǒng)開發(fā)中,使用dotenvy來讀取和使用環(huán)境變量,感興趣的朋友跟隨小編一起看看吧2023-11-11