什么是所有權
什么是所有權
所有程序在運行時都必須管理其使用計算機內存的方式:
- 一些語言中具有垃圾回收機制,在程序運行時有規(guī)律地尋找不再使用的內存,比如C#和Java。
- 在另一些語言中,程序員必須自行分配和釋放內存,比如C/C++。
而Rust則是通過所有權系統(tǒng)管理內存:
- 所有權是Rust最獨特的特性,它讓Rust無需GC就可以保證內存安全,這也是Rust的核心特性。
- 通過所有權系統(tǒng)管理內存,編譯器在編譯時會根據一系列的規(guī)則進行檢查,如果違反了所有權規(guī)定,則程序不能通過編譯。
- 在程序運行時,所有權系統(tǒng)不會減慢程序的運行速度,因為所有權規(guī)則的檢查是在編譯時進行的。
Stack vs Heap
在很多語言中,程序員不需要經常考慮到堆和棧,但在Rust這樣的系統(tǒng)編程語言中,一個值存儲在heap上還是stack上,會很大程度上影響語言的行為,所以這里先對堆和棧進行簡單介紹。
分配內存
棧(stack):
- stack按值的接收順序來存儲,按相反的順序將它們移除,即后進先出(LIFO)。
- 所有存儲在stack上的數據必須擁有已知的固定大小,添加數據叫做入棧,移除數據叫做出棧。
- 將數據存放在stack時不需要尋找用來存儲數據的空間,因為那個位置永遠在stack的頂端。
堆(heap):
- 在編譯時大小未知的數據或運行時大小可能發(fā)生變化的數據,必須存放在heap上。
- 當把數據放入heap時,需要先在heap上分配對應大小的空間,即操作系統(tǒng)在heap中找到一塊足夠大的空間把它標記為在用,并將該空間的地址返回,后續(xù)訪問heap上的數據時,需要通過指針來定位。
訪問數據
- 訪問heap中的數據比訪問stack中的數據慢,因為需要通過指針才能找到heap中的數據,屬于間接訪問。
- 對于現代的處理器來說,由于緩存的緣故,如果指令在內存中的跳轉的次數越少,那么速度就越快。
- stack中數據存放的距離比較近,而heap中數據存放的距離比較遠,因此訪問heap中的數據比訪問stack中的數據慢。
所有權存在的原因
所有權存在的原因,就是為了管理存放在heap上的數據:
- 跟蹤代碼的哪些部分正在使用heap上的哪些數據。
- 最小化heap上的重復數據量。
- 清理heap上未使用的數據,以避免內存泄露。
所有權規(guī)則
所有權規(guī)則
所有權的規(guī)則如下:
- Rust中的每一個值都有一個對應的變量作為它的所有者。
- 在同一時間內,每個值有且只有一個所有者。
- 當所有者離開自己的作用域時,它持有的值就會被釋放掉。
變量作用域
變量作用域
- 作用域(scope)指的是程序中一個項,在程序中的有效范圍。
在下面的代碼中,變量s從第三行聲明開始變得可用,在第五行代碼塊結束時離開作用域變得不可用。如下:
fn main() {
//s不可用
let s = "hello"; //s可用
//可以對s進行相關操作
} //s作用域到此結束,s不再可用
String類型
String類型
為了后續(xù)講解Rust的所有權,我們需要借助一個管理的數據存儲在heap上的類型,這里選擇String類型。
- Rust中基礎的標量類型的數據是存儲在stack上的,而String類型比這些類型更加復雜,它管理的數據是存儲在heap上。
- String類型管理的數據存儲在heap上,因此String類型能夠存儲在編譯時未知大小的文本,即String類型是可變的。
- Rust中有兩種字符串類型,一種是字符串字面值,它是不可變的,另一種就是String類型,其管理的字符串是可變的。
String類型由三部分組成:
- ptr:指向存放字符串內容的指針。
- len:表示字符串的長度。
- capacity:表示字符串的容量。
String類型的這三部分數據存儲在stack上,而String管理的字符串則存儲在heap上。如下:
創(chuàng)建String字符串
創(chuàng)建String字符串可以使用from函數,該函數可以基于字符串字面值來創(chuàng)建String字符串。如下:
fn main() {
let mut s = String::from("Hello");
s.push_str(" String");
println!("{}", s); //Hello String
}
說明一下:
- 代碼中的
::
,表示from是String類型的命名空間下的函數。 - String類型的push_str方法,可以將指定的字符串插入到String字符串的后面。
內存與分配
內存與分配
- 對于字符串字面值來說,在編譯時就知道它的內容了,其文本內容會直接被硬編碼到最終的可執(zhí)行文件中,因此訪問字符串字面值快速且高效。
- 而String類型為了支持可變性,需要在heap上分配內存來保存編譯時未知的文本內容,其必須在運行時向內存分配器請求內存,當我們處理完String時再將內存返回給分配器。
注:在Rust中,當某個值離開作用域時,會自動調用drop函數釋放內存。
變量與數據交互的方式:移動(Move)
在Rust中,多個變量可以采取不同的方式與同一數據進行交互。如下:
fn main() {
let x = 10;
let y = x;
println!("x = {}", x); //x = 10
println!("y = {}", y); //y = 10
}
說明一下:
- 代碼中先將整數字面值10綁定到了變量x,接著生成了變量x的拷貝,并將其綁定到變量y。
- 因為整數是已知固定大小的簡單值,因此x和y都被放入到了棧中,在賦值后兩個變量都有效。
如果將代碼中的整數換成String,那么運行程序將會產生報錯。如下:
fn main() {
let x = String::from("hello");
let y = x;
println!("x = {}", x); //error
println!("y = {}", y);
}
報錯的原因就是我們借用了已經被移動的值x。如下:
現在我們來分析一下代碼,剛開始聲明變量x的時候,整體布局如下:
當把變量x賦值給變量y時,String的數據被拷貝了一份,但拷貝的僅僅是stack上的String元數據,而并沒有拷貝指針所指向的heap上的數據。如下:
當變量離開作用域時,Rust會自動調用drop函數釋放內存,為了避免這種情況下heap上的數據被二次釋放,因此Rust會讓賦值后的變量x失效,此時當x離開作用域時就不會釋放內存。如下:
這就是為什么在賦值后訪問變量x就會產生報錯的原因,因為此時變量x已經失效了。
說明一下:
- stack上的拷貝可以視為淺拷貝,heap上的拷貝可以視為深拷貝。
- 由于深拷貝的成本比較高,因此Rust不會自動進行數據的深拷貝。
變量與數據交互的方式:克隆(Clone)
如果確實需要對String的heap上的數據進行拷貝,那么可以使用String的clone方法。如下:
fn main() {
let x = String::from("hello");
let y = x.clone(); //深拷貝
println!("x = {}", x); //x = hello
println!("y = {}", y); //y = hello
}
拷貝后變量x和變量y都是有效的,因為String的clone方法會將stack和heap上的數據都進行拷貝。如下:
stack上的數據:拷貝(Copy)
- Copy trait可以用在類似整型這樣存儲在棧上的類型,如果一個類型實現了Copy trait,那么舊的變量在賦值給其他變量后仍然可用。
- 任何簡單標量的組合類型都可以實現Copy trait,任何不需要分配內存或某種形式資源的類型也都可以實現Copy trait。
- 所有整數類型、浮點類型、布爾類型、字符類型都實現了Copy,此外,如果元組中所有字段都實現了Copy,那么這個元組也是可Copy的,比如
(i32, i32)
是可Copy的,而(i32, String)
是不可Copy的。
說明一下:
- 如果一個類型或該類型的一部分實現了Drop trait,那么Rust不允許它再實現Copy trait。
- 如果一個類型要實現Copy trait,那么該類型也必須實現Clone trait。
- String賦值后變量會失效,就是因為String沒有實現Copy trait,在賦值時會發(fā)生移動。
所有權與函數
所有權與函數
將值傳遞給函數和給變量賦值的原理類似:
- 對于沒有實現Copy trait類型的變量來說,將值傳遞給函數時會發(fā)生移動,調用函數后變量失效。
- 對于實現了Copy trait類型的變量來說,將值傳遞給函數時會發(fā)生拷貝,調用函數后變量仍然有效。
例如,下面代碼中變量s傳入函數時將發(fā)生移動,后續(xù)不再有效,而變量x傳入函數時將發(fā)生拷貝,后續(xù)仍然有效。如下:
fn main() {
let s = String::from("hello world");
take_ownership(s); //發(fā)生移動
//println!("s = {}", s); //error
let x = 10;
makes_copy(x); //發(fā)生拷貝
println!("x = {}", x); //x = 10;
}
fn take_ownership(some_string: String) {
println!("{}", some_string); //hello world
}
fn makes_copy(some_number: i32) {
println!("{}", some_number); //10
}
返回值與作用域
函數在返回值的過程中同樣會發(fā)生所有權的轉移。如下:
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
代碼說明:
- gives_ownership函數的作用是,創(chuàng)建了一個String,并將其所有權返回。
- takes_and_gives_back函數的所用是,取得了一個String的所有權,然后再將其所有權返回。
引用與借用
引用與借用
- 對于String類型來說,&String就是String類型的引用,我們將創(chuàng)建一個引用的行為稱為借用。
- 一個類型的引用不會取得該類型變量的所有權,因此當引用離開作用域時不會釋放對應的空間。
例如,下面代碼中的calculate_length函數的參數類型是&String,該函數返回傳入String的長度但不獲取其所有權,函數調用后傳入的String變量仍然有效。如下:
fn main() {
let s1 = String::from("hello world");
let len = calculate_length(&s1);
println!("'{}'的長度是{}", s1, len); //'hello world'的長度是11
}
fn calculate_length(s: &String) -> usize {
s.len()
}
實際calculate_length函數的參數s就是一個指針,它指向了傳入的實參s1。如下:
說明一下:
- 與
&
引用相反的操作是解引用,解引用運算符是*
。
可變引用
可變引用
引用和變量一樣默認也是不可變的,要讓引用變得可變,同樣需要使用mut關鍵字。如下:
fn main() {
let mut s1 = String::from("hello world");
let len = calculate_length(&mut s1);
println!("'{}'的長度是{}", s1, len); //'hello world!!!'的長度是14
}
fn calculate_length(s: &mut String) -> usize {
s.push_str("!!!"); //修改了引用的變量
s.len()
}
但可變引用有一個重要的限制就是,在特定作用域內,一個變量只能有一個可變引用,否則會產生報錯。如下:
fn main() {
let mut s = String::from("hello world");
let s1 = &mut s;
let s2 = &mut s; //error
println!("s1 = {}, s2 = {}", s1, s2);
}
Rust這樣做可以在編譯時就防止數據競爭,但可以通過創(chuàng)建新的作用域來允許非同時的創(chuàng)建多個可變引用,因為只要保證同一個作用域下一個變量只有一個可變引用即可。如下:
fn main() {
let mut s = String::from("hello world");
{
let s1 = &mut s;
}
let s2 = &mut s;
}
可變引用的其他限制
Rust中不允許一個變量同時擁有可變引用和不可變引用,否則會產生報錯。如下:
fn main() {
let mut s = String::from("hello world");
let r1 = &s;
let r2 = &s;
let s1 = &mut s; //error
println!("{} {} {}", r1, r2, s1);
}
原因: 不可變引用的要求其引用的值不能發(fā)生改變,而可變引用卻可以改變其引用的值,因此一個變量同時擁有可變引用和不可變引用,就是的不可變引用的作用失效了,但一個變量同時擁有多個不可變引用是可以的。
懸垂引用
懸垂引用(Dangling References)
懸垂引用指的是,一個指針引用了內存中的某個地址,但這塊內存可能已經釋放了。如下:
fn main() {
let r = dangle();
}
fn dangle() -> &String {
let s = String::from("hello world");
&s //懸垂引用
}
在Rust中編譯器確保引用永遠不會變成懸垂狀態(tài),因為編譯器會確保數據不會在其引用之前離開作用域,因此上述代碼會編譯報錯。如下:
說明一下: 報錯內容說缺少一個生命周期說明符,生命周期相關的內容會在后續(xù)博客中講解。
引用的規(guī)則
引用的規(guī)則
引用的規(guī)則如下:
- 在任何時刻,一個變量只能有一個可變引用,或任意數量的不可變引用。
- 引用必須一直有效。
切片
字符串切片
字符串切片
- 除了引用之外,Rust還有另一種不持有所有權的數據類型,叫做切片(slice)。
- 字符串切片就是指向字符串中一部分值的引用,切片形式為:
&字符串變量名[開始索引..結束索引]
。 - 字符串切片中的開始索引指的是切片起始位置的索引值,結束索引指的是切片終止位置的下一個索引值。
- 如果切片的起始位置是0,那么開始索引可以省略,如果切片的終止位置是字符串的長度,那么結束索引可以省略。
- 字符串切片的類型是&str,字符串切片是不可變的。
例如,下面分別創(chuàng)建了字符串hello world
的hello
的切片和world
的切片。如下:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("hello = {}", hello); //hello = hello
println!("world = {}", world); //world = world
}
切片中包含一個指針和一個長度,比如上述的world
切片,其指針指向字符串索引為6的位置,其長度就是5。如下:
切片在Rust中是非常有用的,比如獲取字符串中的第一個單詞,那么借助字符串切片可以編寫出如下代碼:
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
//s.clear(); //error: s已經存在一個不可變引用
println!("word = {}", word); //word = hello
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
說明一下:
- 如果不使用字符串切片,也可以通過返回字符串中第一個空格的索引來間接表示字符串中第一個單詞的位置,但此時這個索引是獨立于這個字符串存在的,當字符串中的內容被清除后這個索引就沒有意義了。
- 而切片是不可變的,因此如果一個字符串存在一個切片,那么在這個切片沒有離開作用域之前,這個字符串中的內容是無法被修改的,因為Rust不允許一個變量同時擁有可變引用和不可變引用,否則會產生報錯。
- as_bytes方法的作用是將String轉化為字節(jié)數組,以方便遍歷String的每一個字節(jié)來與空格進行比較。
- iter方法的作用是在字節(jié)數組上創(chuàng)建一個迭代器,它將會返回字節(jié)數組中的每一個元素,而enumerate方法的作用是對iter的結果進行包裝,將這些元素作為元組的一部分來返回。enumerate返回的元組中,第一個元素是索引,第二個元素是集合中元素的引用。
- 在for循環(huán)中,通過模式對enumerate返回的元組進行解構,由于元組中第二個元素是集合中元素的引用,因此item需要使用
&
。
注意:
- 字符串切片的范圍索引必須發(fā)生在有效的UTF-8字符邊界內。
- 如果嘗試從一個多字節(jié)的字符中創(chuàng)建字符串切片,程序會報錯并退出。
字符串字面值就是切片
字符串字面值的類型實際上就是字符串切片&str
,這就是為什么字符串字面值不可變的原因,因為字符串切片&str
就是不可變的。如下:
fn main() {
let s = "hello world"; //s的類型是&str
}
將字符串切片作為參數
如果要將字符串切片作為函數的參數,那么最好將函數的參數類型定義為&str
,而不是&String
,這樣就能同時接收&String和&str的參數了,能夠使我們的API更加通用且不會損失任何功能。如下:
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string); //接收&String
let my_string_literal = "hello world";
let word = first_word(my_string_literal); //接收&str
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
說明一下:
- &String等價于整個String的切片,因此可以用&str接收,而字符串字面值的類型本來就是&str。
其他類型的切片
其他類型的切片文章來源:http://www.zghlxwxcb.cn/news/detail-722807.html
與字符串切片類似,其他類型也可以有切片,比如對于下面代碼中的數組來說,其切片類型就是&[i32]
。如下:文章來源地址http://www.zghlxwxcb.cn/news/detail-722807.html
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; //&[i32]類型
}
到了這里,關于Rust所有權的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!