計(jì)算機(jī)程序必須在運(yùn)行時(shí)管理它們所使用的內(nèi)存資源。
大多數(shù)的編程語言都有管理內(nèi)存的功能:
C/C++ 這樣的語言主要通過手動(dòng)方式管理內(nèi)存,開發(fā)者需要手動(dòng)的申請和釋放內(nèi)存資源。但為了提高開發(fā)效率,只要不影響程序功能的實(shí)現(xiàn),許多開發(fā)者沒有及時(shí)釋放內(nèi)存的習(xí)慣。所以手動(dòng)管理內(nèi)存的方式常常造成資源浪費(fèi)。
Java 語言編寫的程序在虛擬機(jī)(JVM)中運(yùn)行,JVM 具備自動(dòng)回收內(nèi)存資源的功能。但這種方式常常會(huì)降低運(yùn)行時(shí)效率,所以 JVM 會(huì)盡可能少的回收資源,這樣也會(huì)使程序占用較大的內(nèi)存資源。
所有權(quán)對大多數(shù)開發(fā)者而言是一個(gè)新穎的概念,它是 Rust 語言為高效使用內(nèi)存而設(shè)計(jì)的語法機(jī)制。所有權(quán)概念是為了讓 Rust 在編譯階段更有效地分析內(nèi)存資源的有用性以實(shí)現(xiàn)內(nèi)存管理而誕生的概念。
所有權(quán)有以下三條規(guī)則:
Rust 中的每個(gè)值都有一個(gè)變量,稱為其所有者。
一次只能有一個(gè)所有者。
當(dāng)所有者不在程序運(yùn)行范圍時(shí),該值將被刪除。
這三條規(guī)則是所有權(quán)概念的基礎(chǔ)。
接下來將介紹與所有權(quán)概念有關(guān)的概念。
我們用下面這段程序描述變量范圍的概念:
{ // 在聲明以前,變量 s 無效 let s = "nhooo"; // 這里是變量 s 的可用范圍 } // 變量范圍已經(jīng)結(jié)束,變量 s 無效
變量范圍是變量的一個(gè)屬性,其代表變量的可行域,默認(rèn)從聲明變量開始有效直到變量所在域結(jié)束。
如果我們定義了一個(gè)變量并給它賦予一個(gè)值,這個(gè)變量的值存在于內(nèi)存中。這種情況很普遍。但如果我們需要儲(chǔ)存的數(shù)據(jù)長度不確定(比如用戶輸入的一串字符串),我們就無法在定義時(shí)明確數(shù)據(jù)長度,也就無法在編譯階段令程序分配固定長度的內(nèi)存空間供數(shù)據(jù)儲(chǔ)存使用。(有人說分配盡可能大的空間可以解決問題,但這個(gè)方法很不文明)。這就需要提供一種在程序運(yùn)行時(shí)程序自己申請使用內(nèi)存的機(jī)制——堆。本章所講的所有"內(nèi)存資源"都指的是堆所占用的內(nèi)存空間。
有分配就有釋放,程序不能一直占用某個(gè)內(nèi)存資源。因此決定資源是否浪費(fèi)的關(guān)鍵因素就是資源有沒有及時(shí)的釋放。
我們把字符串樣例程序用 C 語言等價(jià)編寫:
{ char *s = "nhooo"; free(s); // 釋放 s 資源 }
很顯然,Rust 中沒有調(diào)用 free 函數(shù)來釋放字符串 s 的資源(我知道這樣在 C 語言中是不正確的寫法,因?yàn)?"nhooo" 不在堆中,這里假設(shè)它在)。Rust 之所以沒有明示釋放的步驟是因?yàn)樵谧兞糠秶Y(jié)束的時(shí)候,Rust 編譯器自動(dòng)添加了調(diào)用釋放資源函數(shù)的步驟。
這種機(jī)制看似很簡單了:它不過是幫助程序員在適當(dāng)?shù)牡胤教砑恿艘粋€(gè)釋放資源的函數(shù)調(diào)用而已。但這種簡單的機(jī)制可以有效地解決一個(gè)史上最令程序員頭疼的編程問題。
變量與數(shù)據(jù)交互方式主要有移動(dòng)(Move)和克隆(Clone)兩種:
多個(gè)變量可以在 Rust 中以不同的方式與相同的數(shù)據(jù)交互:
let x = 5; let y = x;
這個(gè)程序?qū)⒅?5 綁定到變量 x,然后將 x 的值復(fù)制并賦值給變量 y?,F(xiàn)在棧中將有兩個(gè)值 5。此情況中的數(shù)據(jù)是"基本數(shù)據(jù)"類型的數(shù)據(jù),不需要存儲(chǔ)到堆中,僅在棧中的數(shù)據(jù)的"移動(dòng)"方式是直接復(fù)制,這不會(huì)花費(fèi)更長的時(shí)間或更多的存儲(chǔ)空間。"基本數(shù)據(jù)"類型有這些:
所有整數(shù)類型,例如 i32 、 u32 、 i64 等。
布爾類型 bool,值為 true 或 false 。
所有浮點(diǎn)類型,f32 和 f64。
字符類型 char。
僅包含以上類型數(shù)據(jù)的元組(Tuples)。
但如果發(fā)生交互的數(shù)據(jù)在堆中就是另外一種情況:
let s1 = String::from("hello"); let s2 = s1;
第一步產(chǎn)生一個(gè) String 對象,值為 "hello"。其中 "hello" 可以認(rèn)為是類似于長度不確定的數(shù)據(jù),需要在堆中存儲(chǔ)。
第二步的情況略有不同(這不是完全真的,僅用來對比參考):
如圖所示:兩個(gè) String 對象在棧中,每個(gè) String 對象都有一個(gè)指針指向堆中的 "hello" 字符串。在給 s2 賦值時(shí),只有棧中的數(shù)據(jù)被復(fù)制了,堆中的字符串依然還是原來的字符串。
前面我們說過,當(dāng)變量超出范圍時(shí),Rust 自動(dòng)調(diào)用釋放資源函數(shù)并清理該變量的堆內(nèi)存。但是 s1 和 s2 都被釋放的話堆區(qū)中的 "hello" 被釋放兩次,這是不被系統(tǒng)允許的。為了確保安全,在給 s2 賦值時(shí) s1 已經(jīng)無效了。沒錯(cuò),在把 s1 的值賦給 s2 以后 s1 將不可以再被使用。下面這段程序是錯(cuò)的:
let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); // 錯(cuò)誤!s1 已經(jīng)失效
所以實(shí)際情況是:
s1 名存實(shí)亡。
Rust會(huì)盡可能地降低程序的運(yùn)行成本,所以默認(rèn)情況下,長度較大的數(shù)據(jù)存放在堆中,且采用移動(dòng)的方式進(jìn)行數(shù)據(jù)交互。但如果需要將數(shù)據(jù)單純的復(fù)制一份以供他用,可以使用數(shù)據(jù)的第二種交互方式——克隆。
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
運(yùn)行結(jié)果:
s1 = hello, s2 = hello
這里是真的將堆中的 "hello" 復(fù)制了一份,所以 s1 和 s2 都分別綁定了一個(gè)值,釋放的時(shí)候也會(huì)被當(dāng)作兩個(gè)資源。
當(dāng)然,克隆僅在需要復(fù)制的情況下使用,畢竟復(fù)制數(shù)據(jù)會(huì)花費(fèi)更多的時(shí)間。
對于變量來說這是最復(fù)雜的情況了。
如果將一個(gè)變量當(dāng)作函數(shù)的參數(shù)傳給其他函數(shù),怎樣安全的處理所有權(quán)呢?
下面這段程序描述了這種情況下所有權(quán)機(jī)制的運(yùn)行原理:
fn main() { let s = String::from("hello"); // s 被聲明有效 takes_ownership(s); // s 的值被當(dāng)作參數(shù)傳入函數(shù) // 所以可以當(dāng)作 s 已經(jīng)被移動(dòng),從這里開始已經(jīng)無效 let x = 5; // x 被聲明有效 makes_copy(x); // x 的值被當(dāng)作參數(shù)傳入函數(shù) // 但 x 是基本類型,依然有效 // 在這里依然可以使用 x 卻不能使用 s } // 函數(shù)結(jié)束, x 無效, 然后是 s. 但 s 已被移動(dòng), 所以不用被釋放 fn takes_ownership(some_string: String) { // 一個(gè) String 參數(shù) some_string 傳入,有效 println!("{}", some_string); } // 函數(shù)結(jié)束, 參數(shù) some_string 在這里釋放 fn makes_copy(some_integer: i32) { // 一個(gè) i32 參數(shù) some_integer 傳入,有效 println!("{}", some_integer); } // 函數(shù)結(jié)束, 參數(shù) some_integer 是基本類型, 無需釋放
如果將變量當(dāng)作參數(shù)傳入函數(shù),那么它和移動(dòng)的效果是一樣的。
fn main() { let s1 = gives_ownership(); // gives_ownership 移動(dòng)它的返回值到 s1 let s2 = String::from("hello"); // s2 被聲明有效 let s3 = takes_and_gives_back(s2); // s2 被當(dāng)作參數(shù)移動(dòng), s3 獲得返回值所有權(quán) } // s3 無效被釋放, s2 被移動(dòng), s1 無效被釋放. fn gives_ownership() -> String { let some_string = String::from("hello"); // some_string 被聲明有效 return some_string; // some_string 被當(dāng)作返回值移動(dòng)出函數(shù) } fn takes_and_gives_back(a_string: String) -> String { // a_string 被聲明有效 a_string // a_string 被當(dāng)作返回值移出函數(shù) }
被當(dāng)作函數(shù)返回值的變量所有權(quán)將會(huì)被移動(dòng)出函數(shù)并返回到調(diào)用函數(shù)的地方,而不會(huì)直接被無效釋放。
引用(Reference)是 C++ 開發(fā)者較為熟悉的概念。
如果你熟悉指針的概念,你可以把它看作一種指針。
實(shí)質(zhì)上"引用"是變量的間接訪問方式。
fn main() { let s1 = String::from("hello"); let s2 = &s1; println!("s1 is {}, s2 is {}", s1, s2); }
運(yùn)行結(jié)果:
s1 is hello, s2 is hello
& 運(yùn)算符可以取變量的"引用"。
當(dāng)一個(gè)變量的值被引用時(shí),變量本身不會(huì)被認(rèn)定無效。因?yàn)?quot;引用"并沒有在棧中復(fù)制變量的值:
函數(shù)參數(shù)傳遞的道理一樣:
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() }
運(yùn)行結(jié)果:
The length of 'hello' is 5.
引用不會(huì)獲得值的所有權(quán)。
引用只能租借(Borrow)值的所有權(quán)。
引用本身也是一個(gè)類型并具有一個(gè)值,這個(gè)值記錄的是別的值所在的位置,但引用不具有所指值的所有權(quán):
fn main() { let s1 = String::from("hello"); let s2 = &s1; let s3 = s1; println!("{}", s2); }
這段程序不正確:因?yàn)?s2 租借的 s1 已經(jīng)將所有權(quán)移動(dòng)到 s3,所以 s2 將無法繼續(xù)租借使用 s1 的所有權(quán)。如果需要使用 s2 使用該值,必須重新租借:
fn main() { let s1 = String::from("hello"); let mut s2 = &s1; let s3 = s2; s2 = &s3; // 重新從 s3 租借所有權(quán) println!("{}", s2); }
這段程序是正確的。
既然引用不具有所有權(quán),即使它租借了所有權(quán),它也只享有使用權(quán)(這跟租房子是一個(gè)道理)。
如果嘗試?yán)米饨鑱淼臋?quán)利來修改數(shù)據(jù)會(huì)被阻止:
fn main() { let s1 = String::from("run"); let s2 = &s1; println!("{}", s2); s2.push_str("oob"); // 錯(cuò)誤,禁止修改租借的值 println!("{}", s2); }
這段程序中 s2 嘗試修改 s1 的值被阻止,租借的所有權(quán)不能修改所有者的值。
當(dāng)然,也存在一種可變的租借方式,就像你租一個(gè)房子,如果物業(yè)指定房主可以修改房子結(jié)構(gòu),房主在租借時(shí)也在合同中聲明賦予你這種權(quán)利,你是可以重新裝修房子的:
fn main() { let mut s1 = String::from("run"); // s1 是可變的 let s2 = &mut s1; // s2 是可變的引用 s2.push_str("oob"); println!("{}", s2); }
這段程序就沒有問題了。我們用 &mut 修飾可變的引用類型。
可變引用與不可變引用相比除了權(quán)限不同以外,可變引用不允許多重引用,但不可變引用可以:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
這段程序不正確,因?yàn)槎嘀乜勺円昧?s。
Rust 對可變引用的這種設(shè)計(jì)主要出于對并發(fā)狀態(tài)下發(fā)生數(shù)據(jù)訪問碰撞的考慮,在編譯階段就避免了這種事情的發(fā)生。
由于發(fā)生數(shù)據(jù)訪問碰撞的必要條件之一是數(shù)據(jù)被至少一個(gè)使用者寫且同時(shí)被至少一個(gè)其他使用者讀或?qū)?,所以在一個(gè)值被可變引用時(shí)不允許再次被任何引用。
這是一個(gè)換了個(gè)名字的概念,如果放在有指針概念的編程語言里它就指的是那種沒有實(shí)際指向一個(gè)真正能訪問的數(shù)據(jù)的指針(注意,不一定是空指針,還有可能是已經(jīng)釋放的資源)。它們就像失去懸掛物體的繩子,所以叫"垂懸引用"。
"垂懸引用"在 Rust 語言里不允許出現(xiàn),如果有,編譯器會(huì)發(fā)現(xiàn)它。
下面是一個(gè)垂懸的典型案例:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
很顯然,伴隨著 dangle 函數(shù)的結(jié)束,其局部變量的值本身沒有被當(dāng)作返回值,被釋放了。但它的引用卻被返回,這個(gè)引用所指向的值已經(jīng)不能確定的存在,故不允許其出現(xiàn)。