所有權(quán)
所有權(quán)是Rust最獨(dú)特的特性,它讓Rust無(wú)需GC(Garbage Collection)就可保證內(nèi)存安全。Rust的核心特性就是所有權(quán),所有程序在運(yùn)行時(shí)都必須管理它們使用計(jì)算機(jī)內(nèi)存的方式。有些語(yǔ)言有垃圾回收機(jī)制,在程序運(yùn)行時(shí)會(huì)不斷地尋找不再使用的內(nèi)存。在其他語(yǔ)言中,程序員必須顯式地分配和釋放內(nèi)存。
Rust采用了第三種方式,內(nèi)存是通過(guò)一個(gè)所有權(quán)系統(tǒng)來(lái)管理的,其中包含一組編譯器在編譯時(shí)檢查的規(guī)則。當(dāng)程序運(yùn)行時(shí),所有權(quán)特性不會(huì)減慢程序的運(yùn)行速度。
stack與heap
在像Rust這樣的系統(tǒng)級(jí)編程語(yǔ)言里,一個(gè)值在stack上還是在heap上對(duì)語(yǔ)言的行為和你為什么要做某些決定是有更大的影響的。在你的代碼運(yùn)行的時(shí)候,stack和heap都是你可用的內(nèi)存,但是它們的結(jié)構(gòu)很不相同。
- stack按值的接收順序來(lái)存儲(chǔ),按相反的順序?qū)⑺鼈円瞥ê筮M(jìn)先出,LIFO),添加數(shù)據(jù)叫壓入棧,移除數(shù)據(jù)叫做彈出棧。把值壓到stack上不叫分配。因?yàn)橹羔樖枪潭ù笮〉?,可以把指針存放在stack上。
- 所有存儲(chǔ)在stack上的數(shù)據(jù)必須擁有已知的固定的大小。編譯時(shí)大小未知的數(shù)據(jù)或運(yùn)行時(shí)大小可能發(fā)生變化的數(shù)據(jù)必須存放在heap上。
- Heap 內(nèi)存組織性差一些,一當(dāng)你把數(shù)據(jù)放入heap時(shí),你會(huì)請(qǐng)求一定數(shù)量的空間,操作系統(tǒng)在heap里找到一塊足夠大的空間,把它標(biāo)記為在用,并返回一個(gè)指針,也就是這個(gè)空間的地址。這個(gè)過(guò)程叫做在heap上進(jìn)行分配,有時(shí)僅僅稱為“分配”。
- 入棧比在堆上分配內(nèi)存要快,因?yàn)椋ㄈ霔r(shí))分配器無(wú)需為存儲(chǔ)新數(shù)據(jù)去搜索內(nèi)存空間;其位置總是在棧頂。相比之下,在堆上分配內(nèi)存則需要更多的工作,這是因?yàn)榉峙淦鞅仨毷紫日业揭粔K足夠存放數(shù)據(jù)的內(nèi)存空間,并接著做一些記錄為下一次分配做準(zhǔn)備。
- 訪問(wèn)heap 中的數(shù)據(jù)要比訪問(wèn)stack 中的數(shù)據(jù)慢,因?yàn)樾枰ㄟ^(guò)指針才能找到heap中的數(shù)據(jù)。對(duì)于現(xiàn)代的處理器來(lái)說(shuō),由于緩存的緣故,如果指令在內(nèi)存中跳轉(zhuǎn)的次數(shù)越少,那么速度就越快。
- 如果數(shù)據(jù)存放的距離比較近,那么處理器的處理速度就會(huì)更快一些(stack 上)。如果數(shù)據(jù)之間的距離比較遠(yuǎn),那么處理速度就會(huì)慢一些(heap 上)。在heap上分配大量的空間也是需要時(shí)間的。
- 當(dāng)你的代碼調(diào)用函數(shù)時(shí),值被傳入到函數(shù)(也包括指向 heap 的指針)。函數(shù)本地的變量被壓到stack 上。當(dāng)函數(shù)結(jié)束后,這些值會(huì)從stack 上彈出。
所有權(quán)存在的原因
所有權(quán)解決的問(wèn)題:跟蹤代碼的哪些部分正在使用heap 的哪些數(shù)據(jù);最小化 heap 上的重復(fù)數(shù)據(jù)量;清理heap上未使用的數(shù)據(jù)以避免空間不足。一旦懂了所有權(quán),就不需要經(jīng)常去想stack或heap了,但是知道管理heap數(shù)據(jù)是所有權(quán)存在的原因,這有助于解釋它為什么會(huì)這樣工作。
所有權(quán)規(guī)則
- 每個(gè)值都有一個(gè)變量,這個(gè)變量是該值的所有者。
- 每個(gè)值同時(shí)只能有一個(gè)所有者。
- 當(dāng)所有者超出作用域(scope)時(shí),該值將被刪除。
變量作用域
Scope就是程序中一個(gè)項(xiàng)目的有效范圍。
fn main() {
//s 不可用
let s = "hello";//s 可用
//可以對(duì) s 進(jìn)行相關(guān)操作
}//s 作用域到此結(jié)束,s 不再可用
String類型
String比那些基礎(chǔ)標(biāo)量數(shù)據(jù)類型更加復(fù)雜。字符串字面值:程序里手寫的那些字符串值,它們是不可變的。Rust還有第二種字符串類型:String。在heap上分配,能夠存儲(chǔ)在編譯時(shí)未知數(shù)量的文本。
fn main() {
let mut s = String::from("Hello");
s.push_str(",World");
println!("{}",s);
}
為什么String類型的值可以修改,而字符串字面值卻不能修改,因?yàn)樗鼈兲幚韮?nèi)存的方式不同。
內(nèi)存和分配
字符串字面值,在編譯時(shí)就知道它的內(nèi)容了,其文本內(nèi)容直接被硬編碼到最終的可執(zhí)行文件里,速度快、高效。是因?yàn)槠洳豢勺冃浴?/p>
String類型為了支持可變性,需要在heap上分配內(nèi)存來(lái)保存編譯時(shí)未知的文本內(nèi)容:操作系統(tǒng)必須在運(yùn)行時(shí)來(lái)請(qǐng)求內(nèi)存,這步通過(guò)調(diào)用String::from來(lái)實(shí)現(xiàn)。當(dāng)用完 String之后,需要使用某種方式將內(nèi)存返回給操作系統(tǒng)。這步,在擁有GC的語(yǔ)言中,GC會(huì)跟蹤并清理不再使用的內(nèi)存。沒有GC,就需要我們?nèi)プR(shí)別內(nèi)存何時(shí)不再使用,并調(diào)用代碼將它返回。―如果忘了,那就浪費(fèi)內(nèi)存;如果提前做了,變量就會(huì)非法;如果做了兩次,也是 Bug。必須一次分配對(duì)應(yīng)一次釋放。
Rust采用了不同的方式:對(duì)于某個(gè)值來(lái)說(shuō),當(dāng)擁有它的變量走出作用范圍時(shí),內(nèi)存會(huì)立即自動(dòng)的交還給操作系統(tǒng)。Rust會(huì)在變量超出作用域時(shí)調(diào)用一個(gè)特殊的函數(shù)drop釋放其內(nèi)存。
變量與數(shù)據(jù)交互的方式
1.Move
多個(gè)變量可以與同一個(gè)數(shù)據(jù)使用一種獨(dú)特的方式來(lái)交互。
let x = 5;
let y = x;
整數(shù)是已知固定大小的簡(jiǎn)單的值,這兩個(gè)5被壓到了stack中。
let s1 = String::from("hello");
let s2 = s1;
一個(gè)String由3部分組成:一個(gè)指向存放字符串內(nèi)容的指針,一個(gè)長(zhǎng)度,一個(gè)容量。這些存放在stack中,存放字符串內(nèi)容的部分在heap上,長(zhǎng)度len,就是存放字符串內(nèi)容所需的字節(jié)數(shù)。容量capacity是指String從操作系統(tǒng)歐冠總共獲得內(nèi)存的字節(jié)數(shù)。
當(dāng)把s1賦給s2,String的數(shù)據(jù)被賦值了一份,在stack上復(fù)制了一份指針、長(zhǎng)度、容量,并沒有復(fù)制指針?biāo)赶虻膆eap上的數(shù)據(jù)。當(dāng)變量離開作用域時(shí),Rust會(huì)自動(dòng)調(diào)用drop 函數(shù),并將變量使用的heap內(nèi)存釋放。當(dāng)s1、s2離開作用域時(shí),它們都會(huì)嘗試釋放相同的內(nèi)存,這就是二次釋放(double free)bug。
為了保證內(nèi)存安全,Rust并沒有嘗試復(fù)制被分配的內(nèi)存,而是選擇讓s1失效,當(dāng)s1離開作用域的時(shí)候,Rust不需要釋放任何東西。
如果你在其他語(yǔ)言中聽說(shuō)過(guò)術(shù)語(yǔ)淺拷貝(shallow copy)和深拷貝(deep copy),那么拷貝指針、長(zhǎng)度和容量而不拷貝數(shù)據(jù)可能聽起來(lái)像淺拷貝。不過(guò)因?yàn)镽ust同時(shí)使第一個(gè)變量無(wú)效了,這個(gè)操作被稱為移動(dòng)(move),而不是叫做淺拷貝。隱含的設(shè)計(jì)原則:Rust不會(huì)自動(dòng)創(chuàng)建數(shù)據(jù)的深拷貝,就運(yùn)行時(shí)性能而言,任何自動(dòng)賦值的操作都是廉價(jià)的。
2.Clone
如果真想對(duì)heap上面的String數(shù)據(jù)進(jìn)行深度拷貝,而不僅僅是stack上的數(shù)據(jù),可以使用clone方法。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
3.Copy
let x = 5;
let y = x;
這段代碼似乎與我們剛剛學(xué)到的內(nèi)容相矛盾:沒有調(diào)用 clone,不過(guò) x 依然有效且沒有被移動(dòng)到 y 中。
原因是像整型這樣的在編譯時(shí)已知大小的類型被整個(gè)存儲(chǔ)在棧上,所以拷貝其實(shí)際的值是快速的。這意味著沒有理由在創(chuàng)建變量y后使x無(wú)效。換句話說(shuō),這里沒有深淺拷貝的區(qū)別,所以這里調(diào)用 clone 并不會(huì)與通常的淺拷貝有什么不同。
Rust提供了Copy trait,可以用于像整數(shù)這樣完全存放在stack上面的類型。如果一個(gè)類型實(shí)現(xiàn)了Copy這個(gè)trait,那么舊的變量在賦值后仍然可用;如果一個(gè)類型或者該類型的一部分實(shí)現(xiàn)了Drop trait,那么,Rust不允許讓它再去實(shí)現(xiàn)Copy trait了。
一些擁有Copy trait的類型:任何簡(jiǎn)單標(biāo)量的組合類型都可以是Copy的,任何需要分配內(nèi)存或某種資源的都不是Copy的。
- 所有的整數(shù)類型,例如u32
- bool
- char
- 所有的浮點(diǎn)類型,例如f64
- Tuple(元組),如果其所有的字段都是Copy的
所有權(quán)與函數(shù)
在語(yǔ)義上,將值傳遞給函數(shù)和把值賦給變量是類似的,將值傳遞給函數(shù)將發(fā)生移動(dòng)或復(fù)制。
fn main() {
let mut s = String::from("Hello,World");
take_ownership(s);//s 被移動(dòng) 不再有效
let x = 5;
makes_copy(x);//復(fù)制
println!("x:{}",x);
}
fn take_ownership(some_string: String){
println!("{}",some_string);
}
fn makes_copy(some_number: i32){
println!("{}",some_number);
}
返回值與作用域
函數(shù)在返回值的過(guò)程中同樣也會(huì)發(fā)生所有權(quán)的轉(zhuǎn)移。
fn main() {
let s1 = gives_ownship();gives_ownership 將返回值轉(zhuǎn)移給s1
let s2 = String::from("hello");
let s3 = takes_and_give_back(s2);//s2 被移動(dòng)到takes_and_gives_back 中,它也將返回值移給 s3
}
fn gives_ownship()->String{
let some_string = String::from("hello");
some_string
}
fn takes_and_give_back(a_string:String)->String{
a_string
}
一個(gè)變量的所有權(quán)總是遵循同樣的模式:把一個(gè)值賦給其它變量時(shí)就會(huì)發(fā)生移動(dòng)。當(dāng)一個(gè)包含heap數(shù)據(jù)的變量離開作用域時(shí),它的值就會(huì)被drop函數(shù)清理,除非數(shù)據(jù)的所有權(quán)移動(dòng)到另一個(gè)變量上了。
如何讓函數(shù)使用某個(gè)值,但不獲得其所有權(quán)?
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
但是這傳進(jìn)來(lái)傳出去很麻煩,Rust有一個(gè)特性,叫做引用(references)。
引用
參數(shù)的類型是&String而不是String,&符號(hào)就表示引用:允許你引用某些值而不取的其所有權(quán)。
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()
}
借用
我們把引用作為函數(shù)參數(shù)這個(gè)行為叫做借用。不可以修改借用的變量。和變量一樣,引用默認(rèn)也是不可變的。
可變引用
fn main() {
let mut s1 = String::from("Hello");
let len = calculate_length(&mut s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &mut String) -> usize {
s.push_str(",World");
s.len()
}
可變引用有一個(gè)重要的限制:在特定作用域內(nèi),對(duì)某一塊數(shù)據(jù),只能有一個(gè)可變的引用。這樣的好處是可在編譯時(shí)防止數(shù)據(jù)競(jìng)爭(zhēng)。以下三種行為會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng),兩個(gè)或多個(gè)指針同時(shí)訪問(wèn)一個(gè)數(shù)據(jù),至少有一個(gè)指針用于寫入數(shù)據(jù),沒有使用任何機(jī)制來(lái)同步對(duì)數(shù)據(jù)的訪問(wèn)。我們可以創(chuàng)建新的作用域,來(lái)允許非同時(shí)的創(chuàng)建多個(gè)可變引用。
let mut s = String::from("hello");
{
let r1 = &mut s;
}
let r2 = &mut s;
另一個(gè)限制是不可以同時(shí)擁有一個(gè)可變引用和一個(gè)不變的引用。多個(gè)不可變的引用是可以的。
懸空引用Dangling References
懸空指針(Dangling Pointer):一個(gè)指針引用了內(nèi)存中的某個(gè)地址,而這塊內(nèi)存可能己經(jīng)釋放并分配給其它人使用了。
在Rust里,編譯器可保證引用永遠(yuǎn)都不是懸空引用:
如果你引用了某些數(shù)據(jù),編譯器將保證在引用離開作用域之前數(shù)據(jù)不會(huì)離開作用域。
引用的規(guī)則
在任何給定的時(shí)刻,只能滿足下列條件之一:
- 一個(gè)可變的引用
- 任意數(shù)量不可變的引用引用必須一直有效
引用必須一直有效。
切片
Rust的另外一種不持有所有權(quán)的數(shù)據(jù)類型:切片(slice)。
編寫一個(gè)函數(shù),該函數(shù)接收一個(gè)用空格分隔單詞的字符串,并返回在該字符串中找到的第一個(gè)單詞。如果函數(shù)在該字符串中并未找到空格,則整個(gè)字符串就是一個(gè)單詞,所以應(yīng)該返回整個(gè)字符串。
fn main() {
let mut s = String::from("Hello world");
let wordIndex = first_word(&s);
s.clear();
println!("{}", wordIndex);
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
這個(gè)程序編譯時(shí)沒有任何錯(cuò)誤,但是wordIndex與s狀態(tài)完全沒有聯(lián)系。s被清空后wordIndex仍返回s傳給函數(shù)時(shí)狀態(tài)的值。Rust為這種情況提供了解決方案。字符串切片。
字符串切片
字符串切片是指向字符串中一部分內(nèi)容的引用。形式:
[開始索引..結(jié)束索引]
開始索引是切片起始位置的索引值,結(jié)束索引是切片終止位置的所有值。
let s = String::from("Hello World");
let hello = &s[0..5];
let world = &s[6..11];
let hello2 = &s[..5];
let world2 = &s[6..];
字符串切片的范圍索引必須發(fā)生在有效的UTF-8字符邊界內(nèi)。如果嘗試從一個(gè)多字節(jié)的字符中創(chuàng)建字符串切片,程序會(huì)報(bào)錯(cuò)并退出。
重寫firstworld:
fn main() {
let mut s = String::from("Hello World");
let word = first_word(&s);
//s.clear(); // 錯(cuò)誤!
println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
字符串字面值是切片,字符串字面值被直接存儲(chǔ)在二進(jìn)制程序中。
將字符串切片作為參數(shù)傳遞
有經(jīng)驗(yàn)的Rust開發(fā)者會(huì)采用&str作為參數(shù)類型,因?yàn)檫@樣就可以同時(shí)接收String和&str類型的參數(shù)了:
fn first_word(s: &str) -> &str {
使用字符串切片直接調(diào)用該函數(shù),使用String可以創(chuàng)建一個(gè)完整的String切片來(lái)調(diào)用該函數(shù)。
定義函數(shù)時(shí)使用字符串切片來(lái)代替字符串引用會(huì)使我們的API更加通用,且不會(huì)損失任何功能。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-509026.html
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
let mut s2 = "hello world";
let word2 = first_word(s2);
//s.clear(); // 錯(cuò)誤!
println!("the first word of s is: {}", word);
println!("the first word of s2 is: {}", word2);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
其他類型的切片
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
它跟字符串 slice 的工作方式一樣,通過(guò)存儲(chǔ)第一個(gè)集合元素的引用和一個(gè)集合總長(zhǎng)度。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-509026.html
到了這里,關(guān)于【Rust】所有權(quán)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!