iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
Modern Web

Rust 的戰國時代:探索網頁前端工具的前世今生系列 第 28

Day 28:Rust 中的所有權 (Ownership) 是什麼?(1)

  • 分享至 

  • xImage
  •  

前言

昨天在猜數字遊戲中看到了這段程式碼:

let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("讀取該行失敗");

其中我好奇這個 &mut 的語法如果不寫會怎麼樣?來試著拿掉後編譯看看,會發現印出這樣的錯誤:

error[E0308]: mismatched types
   --> src/main.rs:15:31
    |
15  |         io::stdin().read_line(guess).expect("讀取該行失敗");
    |                     --------- ^^^^^ expected `&mut String`, found `String`
    |                     |
    |                     arguments to this method are incorrect
    |
note: method defined here
   --> /.../rust/library/std/src/io/stdio.rs:398:12
    |
398 |     pub fn read_line(&self, buf: &mut String) -> io::Result<usize> {
    |            ^^^^^^^^^
help: consider mutably borrowing here
    |
15  |         io::stdin().read_line(&mut guess).expect("讀取該行失敗");

這個錯誤會說需要使用 &mut 這樣的 mutably borrowing 的型別,那這個 borrow 又是在借什麼呢?

這就會跟 Rust 其中一個重要的特性 —— 所有權 (Ownership) 有關,而這對像我這樣平常大多寫動態語言的人來說,可能就是學習 Rust 的第一道坎,那以下就跟著我這個 Rust 新手一起來由淺入深吧。

程式管理記憶體的方式

參考文件定義,所有權是指 Rust 中用來管理程式記憶體的一系列規則。在了解是什麼規則前,會需要先學習「程式是如何管理記憶體的」這件事。

rusty

參考這個影片提到的,程式管理記憶體的方式有三種:

  • GC (Garbage collection)
    • 主要被用在各種高階語言中,像是 Java、Python、JavaScript、Go、C# 等
    • 優點
      • 大多情況不會遇到記憶體處理的錯誤,但開發者仍需避免一些該語言中可能造成記憶體使用不當的寫法。像以 JS 而言,沒適時地清掉事件監聽器、clearTimeout 等,久而久之可能會造成記憶體洩漏 (memory leaks) 讓程式卡頓或 crash 掉
      • 因為記憶體管理直接交由每個語言的 GC 去自動處理,學習曲線低、開發起來較快
    • 缺點
      • 沒辦法控制記憶體而造成效能較慢
      • GC 在清理記憶體時可能造成卡頓
  • 手動管理記憶體
    • 像是 C 語言可以使用 malloc (記憶體配置 memory allocation 的縮寫)、free,C++ 可以使用 newdelete 來操作記憶體的配置與釋放
    • 優點:更快的效能,且比起用 GC 的語言有較小的程式大小
    • 缺點:學習曲線高且不好寫,沒寫好的話在執行過程中可能會遇到各種記憶體錯誤
  • 所有權系統
    • Rust 用來確保 memory safety 的記憶體管理系統
    • 優點:結合前述 C、C++ 的優點之外,還能在編譯時就提示錯誤,避免在執行時遇到記憶體錯誤
    • 缺點:學習曲線高

Memory 中的 Stack 與 Heap

記憶體架構

memory

而要了解記憶體管理,另一個重點就是要知道一段執行中的程式的記憶體架構是什麼,這裡參考幾個教學影片與文章整理出上面這張圖,有興趣也可以參考下方的延伸閱讀,首推這個影片這篇文章

下面也大概說明一下記憶體架構,由低位到高位可以分成 4 個區段:

  • Machine code:就是編譯後的讓電腦能讀懂的機器碼,有的影片或文章會直接寫成 Code 或是 Text 來表示
  • Static:程式碼中宣告的常數與全域變數 (global variables)
  • Stack
    • 中文翻作記憶體堆疊,但還是直稱 memory stack 比較能理解
    • 跟資料結構的 stack 不是同個東西,但有沿用其後進先出 (LIFO) 的特性
    • 由各個編譯器在編譯程式時決定一段連續固定大小的記憶體
    • 能存以下資訊:
      • 函式中的 local 變數
      • 函式間呼叫的 call stack 資訊。因此當有太深層的遞迴函式呼叫時會造成所謂的 stack overflow 問題
      • heap 記憶體位址,也就是 C/C++ 中的指標或我們常聽到的 reference
  • Heap
    • 中文翻作記憶體堆積,但堆疊跟堆積常常會搞混,覺得還是保留原文更好記
    • 跟資料結構的 heap 不是同個東西概念也完全不同。資料結構中的 heap 是一個樹狀結構,但記憶體中的 memory heap 指得是一個大型的記憶體池子,裡面有使用中的記憶體、閒置的記憶體
    • 主要用來存動態大小或未知大小的資料,像是配置一段空間給陣列使用
    • 如果使用的程式語言沒有適當地去釋放記憶體或自動做 GC,隨著可用的記憶體逐漸減少,最終會造成卡頓或 crash,也就是所謂的 memory leaks

Stack 與 Heap 的關係

c demo

而 stack 與 heap 的關係,可以直接參考這個影片中的上面這張截圖,雖然範例是 C 語言,但程式碼算蠻單純的應該不會太難懂:

  • 宣告一個 local 變數 a,會被放進 stack 中
  • 宣告一個 p,先到 heap 中配置一段可以存整數大小的記憶體區段,並取得這個區段的記憶體位址 (例如圖上的 200),存到 stack 中,而這個 p 也就是 C 語言中指標的概念,這個變數會指向 heap 中實際存放的 value

假如今天在 C 中沒去用 free 做記憶體釋放,就把 p 指向另一段新的記憶體位址時 (例如圖上的 400),這個 200 區段的記憶體很可能造成浪費。而如果是像前面提到的有 garbage collector 的語言就會定時把這些沒用到的記憶體清掉。

而在 Rust 裡面沒有 garbage collector,它又是如何優雅地管理記憶體的呢,且待下回分解。

延伸閱讀


上一篇
Day 27:Rust 學習筆記 (4) - 用猜數字遊戲來學什麼是 traits
下一篇
Day 29:Rust 中的所有權 (Ownership) 是什麼?(2)
系列文
Rust 的戰國時代:探索網頁前端工具的前世今生30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言