譯者 | 盧鑫旺、云昭
策劃 | Ethan
編程語(yǔ)言各有各的“大能”,但如果談到內(nèi)存管理,Rust的話語(yǔ)權(quán)不是一般的高。GC(垃圾回收)?手動(dòng)分配?對(duì)于掌握了Rust奧義的開(kāi)發(fā)者而言,這些詞匯簡(jiǎn)直弱爆了。眾所周知,Rust編程語(yǔ)言的主要賣點(diǎn)之一是它的內(nèi)存安全性。Rust對(duì)待內(nèi)存,非常有自己的個(gè)性。與使用垃圾收集器的編程語(yǔ)言(如Haskell、Ruby和Python)不同,Rust為開(kāi)發(fā)人員提供了快速功能,能夠以一種獨(dú)特的方式高效地使用和管理內(nèi)存。Rust通過(guò)使用借用檢查器(borrow checker)、所有權(quán)(ownership)、借用(borrow)這三個(gè)概念來(lái)管理和確??缍褩:投训膬?nèi)存安全來(lái)管理內(nèi)存,從而實(shí)現(xiàn)內(nèi)存管理。本文討論了Rust借用檢查器,Rust與其他語(yǔ)言(如Go和C)的內(nèi)存管理對(duì)比,以及Rust借用檢查器的缺點(diǎn)。
內(nèi)存是如何工作的
在討論Rust如何管理內(nèi)存之前,先來(lái)回顧一下計(jì)算機(jī)內(nèi)存是如何工作的。分配給運(yùn)行程序的計(jì)算機(jī)內(nèi)存分為棧和堆。棧是一種線性數(shù)據(jù)結(jié)構(gòu),它按順序存儲(chǔ)局部變量,而不用擔(dān)心內(nèi)存的分配和重新分配。每個(gè)線程都有自己的棧,當(dāng)線程停止運(yùn)行時(shí),每個(gè)棧都會(huì)被釋放。數(shù)據(jù)以后進(jìn)先出(LIFO)的模式存儲(chǔ)——新的數(shù)據(jù)堆積在舊數(shù)據(jù)的上面。堆是一種分層數(shù)據(jù)結(jié)構(gòu),用于隨機(jī)存儲(chǔ)全局變量,內(nèi)存分配和重新分配會(huì)是一個(gè)需要關(guān)注的問(wèn)題。當(dāng)一個(gè)字面量被壓入堆棧時(shí),是會(huì)有一個(gè)確定的內(nèi)存位置的;這使得分配和重新分配(入棧和出棧)很容易。但是,在堆上分配內(nèi)存的隨機(jī)過(guò)程會(huì)導(dǎo)致使用內(nèi)存的開(kāi)銷很大,這使得重新分配內(nèi)存的速度變慢,因?yàn)樵诙焉戏峙鋬?nèi)存時(shí)會(huì)涉及到復(fù)雜的引用記錄。局部變量、函數(shù)和方法駐留在棧上,其他所有變量駐留在堆上;因?yàn)闂S泄潭ǖ挠邢薮笮?。Rust通過(guò)在堆棧中存儲(chǔ)字面量(整數(shù)、布爾值等)來(lái)有效地處理內(nèi)存。像結(jié)構(gòu)體和枚舉這些類型的變量在編譯時(shí)由于沒(méi)有固定的大小,存儲(chǔ)在堆中。
所有權(quán)(所有權(quán)):“值”的主人
所有權(quán)是Rust中的一個(gè)概念,用來(lái)在沒(méi)有垃圾收集器的情況下保證內(nèi)存安全。Rust強(qiáng)制執(zhí)行以下所有權(quán)規(guī)則:
- 每個(gè)值都有一個(gè)變量,稱為owner(所有者)
- 每個(gè)值有且只有一個(gè)所有者
- 如果將變量賦值給新的所有者,那么原始值將被刪除,否則它現(xiàn)在就會(huì)有兩個(gè)所有者
在程序編譯時(shí),Rust編譯器在程序編譯之前會(huì)檢查程序是否遵守了這些所有權(quán)規(guī)則。如果程序遵循所有權(quán)規(guī)則,則程序編譯執(zhí)行,否則編譯失敗。
Rust使用借用檢查器(borrow checker)來(lái)驗(yàn)證所有權(quán)規(guī)則。借用檢查器驗(yàn)證所有權(quán)模型以及內(nèi)存(堆?;蚨眩┲械闹凳欠癯龇秶╯cope)。如果值超出范圍,則釋放內(nèi)存。但這并不意味著訪問(wèn)值的唯一方法是通過(guò)原始所有者。這時(shí)就引出了"借用"的概念了。
借用(借用):重用有術(shù)
為了允許程序重用代碼,Rust提供了借用的概念,和指針類似。
所有權(quán)可以暫時(shí)從所有者處借用,并在借用變量超出范圍時(shí)歸還??梢酝ㄟ^(guò)使用&(&)符號(hào)傳遞對(duì)所有者變量的引用來(lái)借用值。這在函數(shù)中非常有用。下面是一個(gè)例子:
1. fn list_vectors(vec: &Vec<i32>) {
2. for element in vec {
3. println!("{}", element);
4. }
5. }
函數(shù)也可以通過(guò)使用對(duì)變量的可變引用來(lái)修改借用變量。普通變量可以通過(guò)mut關(guān)鍵字將其設(shè)置為可變的,那么可變引用只要在&后添加關(guān)鍵字mut就可以了。當(dāng)然在進(jìn)行可變引用之前,變量本身必須是可變的。
1. fn add_element(vec: &mut Vec<i32>) -> &mut Vec<i32> {
2. vec.push(4);
3.
4. return vec
5. }
左右滑動(dòng)查看完整代碼所有權(quán)和借用的概念可能看起來(lái)沒(méi)有那么靈活,除非你理解了復(fù)制,拷貝,移動(dòng)的概念,以及它們?nèi)绾我黄鸸ぷ鳌?/span>
復(fù)制所有權(quán)
復(fù)制通過(guò)復(fù)制位來(lái)復(fù)制值。復(fù)制僅適用于實(shí)現(xiàn)了Copy特征的類型。一些內(nèi)置類型默認(rèn)實(shí)現(xiàn)Copy特征。在棧中,很容易訪問(wèn)變量并更改所有權(quán),而在堆中復(fù)制則不容易,因?yàn)槲徊僮魃婕拔灰苿?dòng)和位操作,而棧對(duì)于此類操作的組織更有條理。下面是一個(gè)在堆中復(fù)制值的示例。
1. fn main(){
2. let initial = 6;
3. let later = initial;
4. println!("{}", initial);
5. println!("{}", later);
6.
7. }
變量initial和later在同一作用域(范圍scope)中聲明,然后通過(guò)賦值將initial的值復(fù)制到later中。
雖然變量在相同的范圍內(nèi),但initial將不再存在。這是在必須重新分配變量的情況下。輸出:
試圖打印initial變量的值將會(huì)引發(fā)編譯錯(cuò)誤,因?yàn)榻栌脵z查器注意到有變量的所有權(quán)轉(zhuǎn)移了。
那如果你想保留這個(gè)值呢?Rust提供了克隆變量的能力。
拷貝變量
你可以將值分配給新所有者,同時(shí)使用拷貝的方法保留舊所有者中的值。然而,你所拷貝的類型必須提前實(shí)現(xiàn)拷貝特征。
1. fn main(){
2. let initial = String::from("Showing Ownership ");
3. let later = initial.clone();
4. println!("{} == {} [showing successful cloning] ", initial, later)
5. }
變量initial在變量later的聲明中被拷貝,這兩個(gè)變量駐留在堆中。如果這時(shí)被借用,則這兩個(gè)變量將引用同一個(gè)對(duì)象;但是,在這種情況下,這兩個(gè)變量是堆上的新聲明,并占用獨(dú)立的內(nèi)存地址。
移動(dòng)所有權(quán)
Rust提供了跨作用域更改變量所有權(quán)的功能。當(dāng)函數(shù)按值接受參數(shù)時(shí),函數(shù)中的變量會(huì)成為該值的新所有者。如果你不選擇移動(dòng)所有權(quán),可以通過(guò)引用傳遞參數(shù)。下面是一個(gè)如何將變量的所有權(quán)從一個(gè)變量轉(zhuǎn)移到另一個(gè)變量的示例。
1. fn change_owner(val: String) {
2.
3. println!("{} was moved from its owner and can now be referenced as val", val)
4. }
5.
6. fn main() {
7.
8. let value = String::from("Change Ownership Example");
9. change_owner(value);
10. }
change_owner函數(shù)獲得了之前聲明的字符串的所有權(quán),并在接受value變量的值作為參數(shù)時(shí)獲得該字符串的所有權(quán)。此時(shí)試圖打印值變量會(huì)導(dǎo)致錯(cuò)誤。
Rust借用檢查器的缺點(diǎn)
如果Rust的借用檢查器一切都很完美,那么其他系統(tǒng)編程語(yǔ)言可能會(huì)切換或提供帶有借用檢查器實(shí)現(xiàn)的版本。在內(nèi)存管理的問(wèn)題上,它是用戶體驗(yàn)和便利性之間的權(quán)衡。
各主流編程語(yǔ)言的內(nèi)存管理方案一覽
使用垃圾收集器的語(yǔ)言讓內(nèi)存管理變得更容易,但同時(shí)也降低了內(nèi)存管理的靈活性,而像Rust和C這樣的語(yǔ)言讓開(kāi)發(fā)人員可以快速訪問(wèn)內(nèi)存,只要遵守它某些規(guī)則,如Rust的所有權(quán)規(guī)則,以及如何在C中將內(nèi)存管理留給開(kāi)發(fā)人員。
借用檢查器可能是復(fù)雜的和有限制性的。隨著程序規(guī)模的增長(zhǎng),自我確保所有權(quán)規(guī)則可能會(huì)變得困難,并且進(jìn)行更改的代價(jià)可能是昂貴的。雖然Rust編譯器通過(guò)執(zhí)行檢查來(lái)防止類似懸空引用這樣的錯(cuò)誤,但Rust也為開(kāi)發(fā)人員提供了unsafe關(guān)鍵字,可以讓指定代碼區(qū)塊不受檢查。如果外部使用了依賴項(xiàng)unsafe關(guān)鍵字,這可能不利于代碼安全性。許多開(kāi)發(fā)人員,無(wú)論是初學(xué)者還是專家,都會(huì)從借用檢查器中碰到所有權(quán)錯(cuò)誤,更多的錯(cuò)誤來(lái)自于在Rust中實(shí)現(xiàn)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)和算法。
Rust和C的內(nèi)存管理比較
C編程語(yǔ)言是一種流行的系統(tǒng)編程語(yǔ)言,它不使用垃圾收集器或借用檢查器來(lái)管理內(nèi)存;相反,C讓開(kāi)發(fā)人員按照自己的意愿手動(dòng)和動(dòng)態(tài)地管理內(nèi)存。
C開(kāi)發(fā)人員可以使用在標(biāo)準(zhǔn)庫(kù)中定義的malloc()、realloc、free和calloc等函數(shù),用于堆中的內(nèi)存管理,而棧中的內(nèi)存一旦超出作用域就會(huì)自動(dòng)釋放。
哪種方法更好通常取決于要構(gòu)建的內(nèi)容。雖然開(kāi)發(fā)人員可能會(huì)發(fā)現(xiàn)Rust借用檢查器有一些限制,但它使開(kāi)發(fā)人員在管理內(nèi)存時(shí)更加高效,而不需要成為內(nèi)存管理專家。Rust開(kāi)發(fā)人員也可以選擇在沒(méi)有標(biāo)準(zhǔn)庫(kù)的情況下使用Rust,并獲得類似于C語(yǔ)言的體驗(yàn),其中所有內(nèi)存管理都是手動(dòng)來(lái)實(shí)現(xiàn)。
帶有標(biāo)準(zhǔn)庫(kù)和借用檢查器的Rust更適合用于構(gòu)建需要處理資源密集型的應(yīng)用程序。
Rust和Go的內(nèi)存管理比較
Rust和Go是相當(dāng)新的、強(qiáng)大的語(yǔ)言,經(jīng)常在許多方面進(jìn)行比較,包括內(nèi)存管理。
Go使用非分代并發(fā)、三色標(biāo)記和清除垃圾收集器以一種不同的方式管理內(nèi)存,允許開(kāi)發(fā)人員使用new和make函數(shù)手動(dòng)分配內(nèi)存,而垃圾收集器負(fù)責(zé)內(nèi)存回收。
Go的垃圾收集由一個(gè)執(zhí)行代碼并向堆分配對(duì)象的mutator和一個(gè)幫助釋放內(nèi)存的收集器組成。Go還允許開(kāi)發(fā)人員通過(guò)使用不安全的或者運(yùn)行時(shí)包關(guān)閉垃圾收集器來(lái)手動(dòng)訪問(wèn)和管理內(nèi)存。運(yùn)行時(shí)模塊的debug包通過(guò)使用SetGCPercent方法(幫助設(shè)置垃圾收集器目標(biāo)百分比)等方法設(shè)置垃圾收集器參數(shù),為調(diào)試程序提供功能。
Go的垃圾收集器一直以來(lái)在接受來(lái)自Go開(kāi)發(fā)者社區(qū)的批評(píng),并且在過(guò)去的幾年里一直在改進(jìn)。Go開(kāi)發(fā)人員可能希望手動(dòng)管理內(nèi)存,并能從語(yǔ)言中獲得更多,在默認(rèn)情況下,垃圾收集器不允許像C等語(yǔ)言提供手動(dòng)內(nèi)存管理所提供的靈活性。
在討論內(nèi)存管理時(shí),Go和Rust是沒(méi)法比較的,因?yàn)樗鼈冇胁煌?、不相關(guān)的內(nèi)存管理方式,在靈活性和內(nèi)存安全性之間進(jìn)行權(quán)衡,特別是兩種語(yǔ)言的開(kāi)發(fā)人員都想要其他語(yǔ)言使用的東西。
開(kāi)發(fā)人員選擇Go來(lái)構(gòu)建需要簡(jiǎn)單性和靈活性的服務(wù)和應(yīng)用程序,選擇Rust來(lái)構(gòu)建需要低級(jí)別交互,但對(duì)性能和內(nèi)存安全至關(guān)重要的應(yīng)用程序。
借用檢查器:Rust人避不開(kāi)的坎
借用檢查器是Rust之旅中不可繞開(kāi)的困難。學(xué)習(xí)曲線在這里變得相當(dāng)陡峭。伴隨著借用檢查器的接連不斷的報(bào)錯(cuò)、警告,許多具有Python和JavaScript等語(yǔ)言背景的Rust崇拜者難免懷疑人生:“跟借用檢查器硬剛,有前途嗎?,還是放棄吧!”
需要明白的是:任何想要繞開(kāi)借用檢查器的想法都是徒勞的。這是一場(chǎng)你永遠(yuǎn)也贏不了的決斗。唯一能做的,就是將借用檢查器看作是教你如何編寫內(nèi)存效率高的Rust代碼的紀(jì)律制定者,而你必須通過(guò)學(xué)習(xí)更多關(guān)于如何編寫更安全、內(nèi)存效率高的Rust代碼來(lái)玩好跟借用檢查器之間的游戲。
隨著編寫Rust代碼量的增加,開(kāi)發(fā)者當(dāng)然也會(huì)像其他語(yǔ)言一樣,將找到防止出現(xiàn)借用檢查器常見(jiàn)錯(cuò)誤的最佳方法。學(xué)會(huì)與借用檢查器斗智斗勇,開(kāi)發(fā)者避無(wú)可避。
結(jié)語(yǔ)
毫無(wú)疑問(wèn),Rust是一種會(huì)在未來(lái)幾年存在并被廣泛使用的語(yǔ)言。我們已經(jīng)看到像Discord和Microsoft這樣的公司用Rust重寫了他們的一些代碼庫(kù),因?yàn)樗軌蛲ㄟ^(guò)外部函數(shù)接口(FFI)與C和c++等多種語(yǔ)言進(jìn)行交互,還有許多其他公司(如AWS、Mozilla等)在產(chǎn)品的不同環(huán)節(jié)使用Rust。
所有權(quán)和借用是Rust中的基本概念,當(dāng)你編寫更多的Rust程序時(shí),你很有可能會(huì)從借用檢查器中得到一個(gè)錯(cuò)誤。使用合適的工具是很重要的;你可以考慮在內(nèi)存管理不是很重要,并且關(guān)心性能的程序中使用Go。
原文鏈接:
https://stackoverflow.blog/2022/07/14/how-rust-manages-memory-using-ownership-and-borrowing/
譯者介紹
盧鑫旺,51CTO社區(qū)編輯,編程語(yǔ)言愛(ài)好者,對(duì)數(shù)據(jù)庫(kù),架構(gòu),云原生有濃厚興趣,目前就職某跨境電商出海營(yíng)銷公司,擔(dān)任后端開(kāi)發(fā)工作。