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