終於搞明白 Rust 所有權了
大家好,我是螃蟹哥。
所有權是 Rust 的一大特色,也是一大難點。很多人學不下去。本文是一篇難得的好文章,幫助你更好地理解所有權,從內存的角度幫你剖析、講解。希望看完後,你能掌握所有權。
從一個例子開始,看下下面的代碼:
let a: String = "hello world".to_string();
let b: String = a;
QA: a
和b
的變量最終的值是多少?
上面例子通常在有 GC 的語言裏面,根據具體的類型,這種賦值操作會有兩種不同的結果。對於基礎類型,上面的例子會單純的把a
裏面的值直接拷貝到b
裏面,對於複合類型,上面的例子會將b
指向a
,因此它們內部共享了相同的數據。
但是在 Rust 裏面卻不一樣。上面代碼片段a
會被移動到b
,然後a
會被釋放掉,不存在了,也不存儲任何數據。
默認情況下,Rust 會使用 move 來代替 copy。這就意味着 Rust 會逐個字節的 copy 數據,然後移除掉原始的副本,把內存歸還給操作系統。當然也有特殊例子,對於基礎類型,它會 copy 內容,而不會移除原始版本。可見 String 類型不是基礎類型。
內存指針
你也許會思考這些值在內存裏面是如何佈局的?對於不同的類型它們的組織方式有什麼區別?程序是如何區分兩個變量的?
內存可以被看成一個數組,容量大約在 2^64:(64 bit 機器)
let mem: Vec<u8> = vec![0; 18_000_000_000_000_000_000];
(注意:上面語法是 Rust 創建一個 vector,裏面每個元素是 u8 類型,總共有 18000000 個元素,每個元素初始化爲 0)
當你聲明一個變量的時候,程序需要決定將這個變量放在這個內存 list 裏的哪個位置。變量要是在這個 list 裏面的位置,就代表了它對應的內存地址。當然我們可以把另外一個地址的索引存放到這個 list 裏面,這個就是指針 (通常用 & 符號來取地址),通常內存地址是十六進制表示的。
例如我們想要把a
這個變量存儲在0x100
這個地方,我們就可以編寫如下代碼:
let p_a: &i32 = 0x100;
let a: i32 = *p_a;
上面只是一個虛構例子,在這個例子裏面 *p_a
和mem[p_a]
是等價的。因爲內存訪問最小單位是字節,而這個變量有 4 個字節,因此它需要讀取四個索引,因此有時候可以寫成下面這樣:
let a: i32 = mem[p_a] << 24 + mem[p_a+1] << 16 + mem[p_a+2] << 8 + mem[p_a+3] << 0;
注意的是,在這個例子裏面我們將第一個字節對應這個整數的高位地址(這取決於處理器的實現),當然這些細節由程序語言完美的給處理了,包括 Rust,GC。有些操作甚至直接在處理器內部就完成了。
當然我們在寫 Rust/C 的時候不需要這樣編碼,但是理解這些有助於我們理解這些編程語言背後的設計理念,特別是沒有 GC 的語言。
現在我們有一個 4 byte 的變量。當我們存放另外一個 8 byte 變量的時候會發生什麼呢?考慮下面的例子:
let p_a: &i32 = 0x100; // 原始變量
// ---
let p_b: &i64 = 0x102;
let p_c: &i64 = 0x0FA;
let p_d: &i64 = 0x106;
上面三個例子都有一些問題,它們都存放的是 8 字節的變量。p_a
會佔據0x100, 0x101, 0x102, 0x013
四個內存空間,這就意味着p_b
會覆蓋p_a
兩個字節的地址。p_c
同樣也有問題,p_d
不存在這樣的問題,可以正常工作,但是它的問題在於會造成內存碎片。通常情況下,操作系統和編譯器都希望內存儘可能地打包在一起,減少內存之間的空隙。
關鍵點 - 1
-
內存是一個扁平的字節 list。
-
指針在任何編程語言內部都隨處可見
-
編譯器必須知道一個變量在內存裏面佔據多大的位置,以便它們可以正確的讀寫
-
大端還是小端存儲這個在手動操作內存是非常重要的(但是幾乎不需要手動去操作內存)
虛擬內存和初始化
現在讓我們返回內存例子:
let mem: Vec<u8> = vec![0, 18_000_000_000_000_000_000];
你也許會很好奇,這個例子裏面給這個 vector 64 個元素,這個幾乎比任何實際中機器的內存都要大(幾乎,因爲有些內存地址會被保留)。
或者換另外一個問題更好,不同的程序它們都運行在同一個內存空間裏面麼?在這種情況下,一個程序需要避免和另外一個程序產生衝突(上面例子的變量覆蓋問題);答案通常是否定的,但是取決於具體情況。事實上程序裏面看到的0x100
並不是實際物理內存的地址。這個是虛擬內存方面的東西,在一個 x86_64 機器上,一個程序能看到的內存大小是 2^64 字節,即使機器只有 4 個 G 的內存空間。(不考慮特殊的操作系統)
下面一個問題就是:在內存裏面變量會不會被初始化,或者你認爲的是會被初始化爲 0 或者一些過時的數據?事實上都有可能,這個取決於操作系統和它的具體配置。所以永遠不要假設數據會被初始化爲 0 或者是垃圾數據。初始化爲 0 是爲了避免將內存信息泄漏給其他的程序。大部分的 GC 語言,例如 Go 語言會將內存初始化爲 0。大部分的非 GC 語言例如 GC,Rust 會原封不動的保留初始化的內存信息,這個需要人爲手動的去初始化它。
Rust 其實是允許讀取未初始化的內存的,但是僅限於unsafe
。這個主要是爲了避免在一些複雜的算法裏面初始化兩次。GC、Rust 都比較相信程序員知道他們自己在做的事情,但是和 GC 不同的是,Rust 有很多的安全策略,而 GC 則需要程序員時刻小心謹慎。(當然 Rust 也提供了 unsafe 編程,但是這個僅針對專家級別的)。
關鍵點 - 2
-
程序運行在虛擬內存裏面(虛擬內存比實際內存大得多)
-
內存通常不會被初始化,裏面是一些隨機的數據,但是有些操作系統會爲了安全考慮初始化掉內存
-
編程語言一般要麼初始化內存(一些 GC 語言),要麼阻止你讀取未初始化的內存(safe-Rust)
內存分配和釋放
這是討論內存的最後一個主題。假設你寫了下面的代碼:
let p_a: &i32 = 0x100;
let a:i32 = *p_a;
上面代碼其實在 Rust 裏面是無法工作的,對應的 C 代碼如下:
int &p_a = 0x100;
int a = *p_a;
Rust 是不允許我們直接操作內存的(除非我們使用 unsafe)。對應的 C 代碼即使可以編譯,它也是不能正常工作的。
主要原因是p_a
指向了一個未分配的內存(未分配的內存和未初始化的內存不是一個概念);程序是無法讀取任何指向未分配內存的地址的。這個是由 OS 來保證的。在 C 裏面可以通過alloc/malloc
來分配內存。當一個程序申請一段內存的時候它不需要具體指派內存地址,只需要告訴操作系統我需要多大的內存就可以了。這個地址是由操作系統分配的,程序無法干預。
int &p_a = (int*) malloc(sizeof(int));
內存在分配後就可以被讀寫了,但是需要注意的是,具體的內存裏面的內容是什麼我們不清楚,這個取決於操作系統(除非我們手動的去初始化)。
當我們利用完了這段內存,並且不再需要它的時候,我們需要釋放掉它,並且把它歸還給操作系統(這相當於告訴操作系統,這段內存我已經用完了,你可以回收了給其他程序用了)。如果我們不去釋放它,那麼就會造成內存泄漏問題。
在 Rust 裏面我們不需要擔心內存的分配和釋放問題了,因爲 Rust 已經幫我們管理了。我們不太需要擔心內存泄漏的問題了,除非因爲數據結構遞歸,當然這個是其他任何 GC 裏面都可能會有的問題。這個和 C 語言是有點不一樣的,在 C 語言裏面我們需要通過 free()
手動釋放它 ,當然在 Rust 裏面我們也可以手動的去釋放drop(a)
。之前已經提到了在 Rust 裏面其實不太需要關注這個,當然如果想要提前釋放的話是可以的。(在其他類似 C 語言裏面,這個裏面處理不當還會有二次釋放的問題)
關鍵點 - 2
-
在內部,內存的使用是需要向操作系統申請,不用的時候需要歸還給操作系統的
-
程序無法決定被分配的地址
-
內存釋放是必要的,這樣可以避免內存泄漏的問題,Rust 已經幫忙做了
-
Rust 裏面儘量別用 unsafe,unsafe 是爲專家準備的
Stack 和 Heap
之前提到的內存分配 / 釋放都是針對 heap 的。當然還有 stack 內存空間,這裏不打算探討它們的區別。在 Rust 裏面主要傾向於基於作用域來自動分配 / 釋放內存來避免 GC。Rust 會將一部分的數據放在 heap 上面,並且大部分的變量放在 stack 上面。例如Box<T>
會把數據存放在 heap 上面。
複合類型在內存的佈局
探討下複合類型內部在內存裏面是如何佈局的。在 C++ 裏一個對象看起來像下面這個樣子:
class Card {
public:
int number;
int suit;
};
int main(){
Card aceOfSpades;
aceOfSpades.number = 1;
aceOfSpades.suit = 4;
}
在 Rust 裏面如下:
pub struct Card{
pub number: i64,
pub suit: i64,
}
fn main(){
let ace_of_shades = Card{
number:1,
suit: 4,
};
}
其他語言類似,如 Python:
class Card:
number: int
suit: int
ace_of_shades = Card
ace_of_shades.number = 1
ace_of_shades.suit = 4
但是注意,大多數語言其實都是允許我們先創建對象,而不需要對對象裏面的內存先賦值,因此我們後續可以對對象裏面內容再做修改。但 Rust 不行,還記得之前內存初始化裏面談及到的,Go、Python(或者大部分的語言)都會將分配的內存直接初始化爲 0,因此這就意味着我們在創建對象的時候指定它們具體的值的時候,其實是對這個對象裏面的字段做了兩次賦值。這種事情在大部多數語言裏面通常都不是性能的問題,而是缺少窮舉性:如果你忘了給某些變量定義一個值,那麼它默認會給你初始化爲 0,編譯器不會給你任何警告(可能這個 0 並不是我們期望的,這個在後續就很難 debug)。但是這個在 Rust 裏面是不允許發生的。
現在我們迴歸到最初的問題,那麼這樣一個複合類型在內存裏面是如何佈局的呢。很簡單,它使用了 16 個字節空間,前 8 個字節存儲number
,後 8 個字節存儲suit
,連續的 16 個字節,看起來非常好, 這個對象在編譯期間就已經知道大小了。
這就意味着:
&ace_of_shades.suit = (&ace_of_shades+ 8)
現在讓我們討論下字符串類型,String 類型是多大呢?如果編譯器要去讀取 string,那麼它該從內存裏面讀取多少個字節呢?現在的問題是 string 是可以任意大小的。它的大小可以是一個空的字符串,也可以是在一個變量裏面存儲一本書的內容,那麼該如何存儲他們呢?在 C 語言裏面,string 可以通過終止符 0x00 來分割,因此函數必須要一直的讀,直到檢測到這個終止字符爲止。但是這個有個缺點,如果在你字符串中間有個\0
, 那麼你可能就不能完整地讀取他們了。在 Rust 裏面一個 String 類型,它有一個指向數據底層的指針和一個它的長度:
pub struct String {
buf: *mut u8,
len: usize,
}
需要注意的時候,String 在 Rust 裏面和 Vec 類型是一樣的,但是 Vec 本身使用了類似的機制,在這裏我故意忽略它的容量,容量會佔 8 個字節。
這種結構就可以使得 String 變得 sized,在 64 機器上面,它有 16 個字節的長度,忽略 String 本身的內容,它的長度基本是固定的。當然這並不意味着它永遠佔據 16 個字節,很顯然保存文本的內存仍然要被使用,但是它們存儲在另外的地方。和 C 語言相比它幾乎沒啥缺點,除了它會浪費 16 個字節,而 C 語言只需要浪費 1 個字節。
那麼方法呢?方法 / 函數在代碼段。
關鍵點
-
對象通常存儲在內存裏面,通過字段來鏈接他們。
-
對象也可以存儲指向內存裏面的一個地址
-
方法在 object 裏面不會佔據空間
所有權(OwnerShip)
Ownership 這個也許是 Rust 裏面最重要的一個概念了,想象下,當我們在一系列函數之間傳遞數據,那麼這個數據應該在哪個地方被分配內存,在哪個地方被釋放內存。在 GC 語言裏面 GC 會幫我們做這些事情,但是這個會消耗一些時間週期。C/C++ 有個很酷的方式,那就是利用 stack 和 scope。例如下面的代碼:
int main()
{
Card aceOfSpades;
aceOfSpades.number = 1;
aceOfSpades.suit = 4;
}
上面變量 aceOfSpades 會被自動創建,當離開作用域的時候會被自動釋放,cool?對於這樣簡單的例子,C 自動幫我們管理了內存。如果這種簡單的例子可以擴展到其他任意的場景下面,那麼就很好了,因爲對象會在最裏面的函數被丟棄掉。如果一個函數能夠在使用數據之後是否要釋放數據,那麼就更好了。但是問題是如果一個函數釋放了數據,有其他調用者期待這個數據仍然存活,那麼就會有報錯。
但是這個在 C++ 裏面是很難的一件事情,有些人可能使用函數命名模式來傳達這一點,或者直接乾脆全部 clone/copying 來代替傳遞指針來避免風險。
在 Rust 裏面,這個是由編譯器來強制,編譯器通過追蹤誰擁有這個數據,對數據擁有所有權的那個變量負責數據的釋放,要保證在任意一個時間點只有一個 owner,如果不是,那麼就有可能會有多次釋放的危險。在 Rust 裏面當你創建了一個變量,那麼你就擁有了這個變量:
fn main(){
let ace_of_shades = Card{
number: 1,
suit: 4,
};
}
在這裏,main 擁有 ace_of_shades,並且它在最後需要負責這個數據的釋放,因此在代碼的末尾有一個隱式的drop(ace_of_shades)
。
目前爲止這個基本上和其他語言沒有啥區別,但是 Rust 有個很酷的功能,那就是 Ownership 是可以轉移的。
fn main(){
let ace_of_shades = Card {
number: 1,
suit: 1,
}
print_card(ace_of_shades);
}
fn print_card(card: Card){
println!("Number: {} Suite: {}", card.number, card.suit);
}
在上面的例子裏面print_card
就接受了 Card 的所有權,並且在它函數的最後會釋放掉它,這就意味着 main 不再需要負責釋放它的內存了。但是,Rust 是如何知道在函數的最後它應該要釋放掉內存?因爲這個函數接受的是Card
而不是&Card
或者&mut Card
, 如果你看到的是完整的類型,而不是有個 &, 那麼它就對這個值是擁有所有權的。對於&Card
和&mut Card
這兩種意味着函數對指向這段內存空間的地址擁有所有權,而不是指針指向的內容擁有所有權。相同的形式,Rc<T>
或者Box<T>
這樣的參數,那麼函數擁有的所有權屬於外部的 Rc 或者 Box,但是裏面的行爲怎麼樣取決於具體的類型。
當我們不想要這個類型被 drop 掉呢?有兩種方式,第一種改變接受者,接收一個借用的對象,或者在發送數據之前拷貝一份,類似print_card(card.clone())
;, 在 Rust 裏我們需要實現 clone 這個 trait 或者單純的在 structure 上面加個#[derive(Clone)]
來實現默認的 copy 行爲,默認的 copy 會拷貝所有的數據。所以我們可以看到在 Rust 裏面默認的行爲是 move, 而在 C++ 裏面默認的行爲是 copy。如果我們在 Rust 裏面不想要在內部函數里面, 那麼就需要在 card 前面增加一個 & 符號,fn print_card(card: &Card){}
。這個是告訴編譯器,它不擁有這個數據,它無權釋放數據,這個時候card
就會被當成一個指向Card
數據的指針,當丟去這個變量的時候,它丟棄的只是指向底層數據的指針。其餘的代碼不需要做任何的改動。
通過這些簡單規則 Rust 很容易可以做出決定,它應該在什麼時候分配內存,什麼時候清理內存,我們只需要關心 owner 和 borrow, 但是這些是由編譯器來檢查的,沒有辦法去欺騙他。當然還有個比較不好的方法,通過返回所有權來達到這個效果:
fn main(){
let ace_of_shades = Card{
number: 1,
suit: 1,
};
let ace_of_shades = print_card(ace_of_shades);
}
fn print_card(card: Card) -> Card {
println!("Number: {} Suite: {}", card.number, card.suit);
card
}
上面的例子有點滑稽,其實可以依賴編譯器來避免這種二次移動,其次這個在 Rust 裏面是一種反模式,這種弊大於利,往往會帶來更多的問題。根據一般經驗來看,如果你不需要在你的函數里面釋放這個對象,那麼就最好不要獲取他的所有權。
ownership 和 borrowing 我們可以看成是 Rust 裏面的權限系統(這裏權限不是用戶權限,不是讀寫權限,是編譯器 / 內存管理權限,有沒有權利去釋放資源,mut 控制讀寫權限)。
-
所有者擁有最高權限。它可以隨心所欲地處理數據,因爲這是他們的數據。如果我有自己的車,我可以隨心所欲地使用它,包括處理它。總有一位車主,因爲這輛車需要向某人登記。
-
&mut: 第二權限,可以修改內容,但是不能釋放內容。
-
& (即借用)最低權限,只能讀,不能改,更不能釋放。
有一點需要注意的是,其他人可以對 borrow 做 copy 操作。例如一個函數需要對 &Card 做修改,那麼可以先 copy,再修改 copy 的數據。
fn main(){
let ace_of_shades = Card{
number: 1,
suit: 1,
};
let ace_of_shades = print_card(ace_of_shades);
}
fn print_card(card: Card) -> Card {
println!("Number: {} Suite: {}", card.number, card.suit);
let mut next = card.clone();
next.number += 1;
next
}
這套體系的比較好的在於開發人員一開始就會知道函數是不是會對我們傳入的數據做修改。
關鍵點
-
正常在 Rust 裏面通常是 owner 的,除非在類型前面加 & 或 &mut 。
-
ownership 意味着在對象離開作用域後,它有權釋放這個對象
-
一個對象永遠有且只有一個 Owner。
-
copy 和 clone 與其他語言裏面的實現是一樣的,沒有其他花裏花哨的東西
-
borrow 是兩個函數共享對象一個非常好的辦法,如果我們不希望在使用後釋放它的話。
-
當我們創建一個函數的時候,我們優先考慮用最低權限
-
我們永遠可以修改一個數據的副本。是不是需要修改原始數據我們需要小心,也許不需要。
可複製的類型
在 Rust 裏面,copy 是有着特殊含義的,你也許會注意到當我們討論 copy 和 clone 的時候,我們是把他們當作兩件不同的事情來看待的。其實在 Rust 裏面他們本來就是兩件不同的事情,copy 是逐字節拷貝的,而 clone 則是由用戶自定義來實現的。下面我們先只關注 copy,先忘掉有 clone 這個玩意。
讓我們考慮下面這個例子:card 在內存裏面的 layout 如下
copy 意味着我們程序會從內存裏面讀取這些字節,然後把它們放在另外一個地方。在拷貝的那一刻,它不在意它是 card 還是其他什麼,它只是一個需要被拷貝到其他地方的大小是 16 個字節的數據。
那麼現在的問題是,這種 copy 是不是總是 work 的,總是保持數據的準確性。
對於常規的 value,例如 number 或者 char 類型,它無疑是正確的,那麼對於內存地址呢?如果它包含了一個內存地址,那麼它會發生什麼?如果這個指針是一個共享只讀的 borrow,那麼它不會有任何問題,我們可以有任意多的 reader,它仍然工作的非常好(因爲指針指向的內存不會變), 所以它一定是 work 的。
對於可變的借用,唯一的問題就是會有多個讀寫指針指向同一個內存地址(這個會造成數據競爭),除此之外沒有其他的問題,這個指針仍然是合法的。但是 Rust 不允許複製可變的借用,以便防止打破規則。
因此在 Rust 裏面存在着一種狀況,有些數據可以 copy,有些數據不允許 copy。但是。。。有一種可能那就是指針本身就是結構體的一個對象(結構體擁有對該指針的所有權), 例如下面:
pub struct String{
buf: *mut u8,
len: usize,
}
buf 實際所有權歸屬於 String,當創建一個新的 String 的時候,內存會隨着 buf 被保留,當 String 被釋放掉的時候,buf 裏面的內容也會被釋放掉。我們也許會猜測這個可能是 Rust 編譯器幫我們做的這些事情,那麼我們可以人爲的來完成這些事情麼?
上面問題答案是 yes,這個主要是由Box<T>
來完成的,這種結構一般用來存儲指向一個內存位置的指針(或者存儲一個相同結構的指針)。例如下面例子:
pub struct HandOfCards{
card1: Box<Card>,
card2: Box<Card>,
card3: Box<Card>,
card4: Box<Card>,
card5: Box<Card>,
}
上面這個例子並不是一個實際的例子,只是拿來演示,在這個例子裏面Card
內存需要被提前分配,在我們創建 HandOfCards,當 HandOfCards 退出作用域的時候,Card 也會被釋放掉。如果我們存儲在 vec 裏面,vec 機制和 box 差不多,當 vec 被釋放的是時候,card 也會被釋放掉。
現在我們開始返回 Copying,如果我們嘗試 copy String 這個結構體,那麼問題是我們拷貝的是指針,而不是裏面實際的內容,這就意味着實際的數據有兩個指針同時指向它, 這個不僅是破壞這個規則,同時還是帶來二次釋放的問題,因爲 box 被釋放掉的時候, box 裏面的數據也會被釋放。
那麼哪些數據是可以被 copy 的?只有實現了Copy
這個 trait 的類型纔可以被 copy,如果 String 結構體實現了 copy 這個 trait,那麼它也是可以 copy 的。impl Copy for Card{}
, 現在這個結構體就可以被 copy 了,這個是 Rust 幫忙我們做的,我們不需要考慮,這個是該類型可以 copy 的唯一的方式,當然我們通常使用下面的寫:#[derive(Copy)]
。
關鍵點
-
copy 在 Rust 只是 byte by byte, 它不會去試圖理解類型的內容
-
內存在某些情況下會阻止 copy,因爲結構體裏面某些字段會禁止它們被 copy
-
不可變借用是可以 copy 的,可變借用則不允許
-
如果有可能則儘量實現 copy 這個 trait,因爲它會使得你更容易編寫一些東西
copy 和 clone 的異常
一開始討論 copy 的時候提到暫時忽略 clone。當我們在談到 clone 的時候這個裏面會有一些小問題。
首先對於 C++ 來講它不總是會去 clone,clone 對於 C++ 來講是 deep copy。對於簡單的類型,它默認就是 copy,對於複雜的類型,則取決於它具體的實現。clone 有很多弊端,首先它會浪費很多的(CPU)週期,對於簡單的類型和 copy 沒啥區別,對於複雜的類型,它會浪費更多的資源 (cpu / 內存),和 copy 一樣,想要 clone 則需要實現 clone 這個 trait。
通過傳遞引用,而不是傳遞值,則會幫我們減少不必要的 copy。總是 clone 和 copy 也會帶來一些問題,比如你想要改變一個值,但是你把這個改動寫到了一個你不期望的副本里面。在這種情況情況下可以使用&mut
或者Rc<T>
也許會幫助到你。
在某些情況下實現 copy 不是一個好主意,它會引起一些意想不到的意外。例如當你創建了一個迭代器,你可能會以不同的迭代器結束,而不是一開始的那個迭代器。故意不對這些類型實現 copy,這是作者向我們傳達這些類型應該如何被合理使用的一種方式。
copy,Rust 編譯器會幫我們做,clone 需要自定義,請在合適的情況下使用 clone 或 copy。
move 語義
在 Rust 裏面。所有的類型都是可 move 的,move 意味着只要最初的版本被銷燬,創建的副本就必須有效。這就意味着我可以獲取任何類型,然後通過 copy 數據的方式將它的內存地址從 0x100 改變成 0x200,然後移除它原始的版本,它應該還是正常的 work 的。當然這個前提是這個類型裏面沒有指向初始數據的指針,否則就會造成懸浮指針的問題,這就意味着這個類型裏面沒有借用,無論是可變還是不可變的。也就是說如果需要移動,必須要有所有權。
當然也有特例情況,std::mem:swap
這個方法接受兩個 &mut 指針,然後移動它們的數據,當然對於同一個類型的兩個實例,切換它們的內容這個就應該是合理的。
問題來了,對於所有任何類型這個都是 work 的麼?我們可以放任何類型在 structur e 裏面,它仍然 work 麼?幾乎是,但有一種情況下它會失敗, 如果你構建了一個自引用的結構體,那麼它就會失敗。
例子如下,假設你有個結構體,裏面有個字段有自己數據 (ownership),但是通過一個指向其成員的指針給用戶暴露一個只讀接口,這個接口主要防止別人不經過某些方法而直接修改它。
struct MyData {
buf: Vec<u8>,
pub buffer: &Vec<u8>, // this is always point to &buf
}
這個結構體只要移動,就會被破壞掉。當它移動的時候,MyData 的內存地址就會被移動,那麼 buffer 就會指向一個過時的地址,這個顯然是有問題的。(NewMyData.buffer = &OldMyData + 0, 而這個地址是非法的。), 這個變量依賴這個結構體在內存的位置。
正因爲這個原因,Rust 是不允許在結構體裏面構建自引用的。這個是通 過 borrow lifetime 來禁止的, 你需要在結構體生命週期裏面保證它的存在,但是又不能通過這種方式將它和結構體生命週期綁定起來。
如果非要這麼做,可以通過在一個方法裏面返回一個 borrow pointer 來做到這個效果,而不需要通過將指針存儲到內存裏面這種方式。
在 Rust 如果代碼有可能是不安全的,那麼 Rust 就不支持。除非通過 unsafe 來做,unsafe 裏面有個 Pin 的東西。
關鍵點
-
Rust 裏面所有的 type 都是可 move(move 意味着改變它們自身的內存地址)
-
mem:swap 可以用於兩個 &mut 指針(因爲正常的 move 必須要有 ownership)
-
在安全的 Rust 裏面,自引用不支持,除非用 unsafe
-
當然避免它們可以通過 Rc 來做到。
回到開始的問題
現在回過頭來看看開始的問題,你的疑惑是否沒有了?
let a: String = "Hello world".to_string();
let b: String = a;
現在如果我問你這裏發生了什麼,它應該更容易推理。首先,這會創建一個新的 String 類型並將其存儲到 “a” 中。然後,因爲 String 不能被複制(它必須包含對文本緩衝區的擁有引用),所以只能移動(move)它。因此,Rust 將 “a” 的內容複製到 “b” 並刪除 “a” 的內存。沒有其他方法可以使這成爲可能。
(注意:這種情況太簡單了,Rust 移動數據沒有意義。它會將 “b” 指向 “a” 的地址,而忘記“a”。這是內部許多優化之一 Rust 將發揮作用以將生成的代碼減少到絕對最小值)
你想要兩個不同的副本?可以這麼做:
let b: String = a.clone();
你想讓兩個變量指向同一個地方,而不需要使用兩倍的內存?當然!
let b = &a;
一旦我們瞭解了幕後發生的事情,行爲就會變得不言而喻,對嗎?
如果我們有一個數字而不是字符串,它們是可複製的,所以在這種情況下,Rust 將複製而不是丟棄(drop):
let a: i64 = 123;
let b: i64 = a;
dbg!(a, b); // This will now print both variables
我希望本文有助於理解爲什麼 Rust 會這麼實現。
基於英文 Rust – What made it “click” for me (Ownership & memory internals):https://deavid.wordpress.com/2021/06/06/rust-what-made-it-click-for-me-ownership-memory-internals/,有刪減和自己的理解、處理。感興趣的可以認真閱讀該英文文章。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1BGdAJ0GQio800z4uBFLnw