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