iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Rust

30天Rust從零到全端系列 第 8

Day 8: 理解 Rust 所有權系統與記憶體管理 (更新)

  • 分享至 

  • xImage
  •  

前言

恭喜你完成了第一部分的學習!經過前七天的基礎訓練,我們已經掌握了 Rust 的基本語法和 Cargo 工具鏈。今天,我們要進入所有權系統(Ownership System)

但為什麼需要所有權系統?

在開始之前,讓我們先理解為什麼 Rust 需要這套獨特的系統:

傳統記憶體管理的兩種方式

  1. 垃圾回收(Garbage Collection)

    • 語言:Java、Python、Go
    • 優點:自動管理,開發簡單
    • 缺點:效能開銷、不可預測的暫停
  2. 手動管理

    • 語言:C、C++
    • 優點:完全控制、高效能
    • 缺點:容易出錯(記憶體洩漏、雙重釋放、懸垂指標)

Rust 選擇了第三條路:編譯時期的記憶體管理,透過所有權系統在編譯時就確保記憶體安全。

所有權的三大規則

// 規則 1:每個值都有一個擁有者(owner)
// 規則 2:同一時間只能有一個擁有者
// 規則 3:當擁有者離開作用域,值會被丟棄

fn main() {
    {                      // s 在這裡無效,它尚未聲明
        let s = "hello";   // s 從這裡開始有效
        
        // 使用 s
        println!("{}", s);
    }                      // 作用域結束,s 不再有效
}

深入理解所有權轉移(Move)

基本型別的複製

fn main() {
    // 基本型別實作了 Copy trait,會自動複製
    let x = 5;
    let y = x;  // x 的值被複製給 y
    
    println!("x = {}, y = {}", x, y);  // 兩個都可以使用!
}

String 的所有權轉移

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的所有權轉移給 s2
    
    // println!("{}", s1);  // 錯誤!s1 已經無效
    println!("{}", s2);     // 只有 s2 可以使用
}

讓我們視覺化這個過程:

堆疊(Stack)         堆積(Heap)
┌─────────┐         ┌─────────────┐
│   s1    │ ───X──> │  "hello"    │
├─────────┤         └─────────────┘
│   s2    │ ───────────────┘
└─────────┘

函式與所有權

所有權的傳遞

fn main() {
    let s = String::from("hello");
    
    takes_ownership(s);  // s 的所有權移動到函式中
    
    // println!("{}", s);  // 錯誤!s 已經無效
    
    let x = 5;
    makes_copy(x);  // x 是 i32,會複製
    
    println!("x = {}", x);  // x 仍然有效
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}  // some_string 離開作用域並被丟棄

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}  // some_integer 離開作用域,沒什麼特別的

返回值與所有權

fn main() {
    let s1 = gives_ownership();  // 函式返回值的所有權移動給 s1
    
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);  // s2 移動到函式,返回值移動給 s3
    
    println!("s1 = {}, s3 = {}", s1, s3);
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string  // 返回並移動所有權
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string  // 返回並移動所有權
}

實戰練習:任務管理系統

讓我們用昨天學到的 Cargo 建立一個新專案,實踐所有權概念:

cargo new task_manager
cd task_manager

版本 1:所有權問題

// src/main.rs
#[derive(Debug)]
struct Task {
    title: String,
    completed: bool,
}

fn main() {
    let task = Task {
        title: String::from("學習 Rust 所有權"),
        completed: false,
    };
    
    process_task(task);
    
    // println!("{:?}", task);  // 錯誤!task 已經被移動
}

fn process_task(task: Task) {
    println!("處理任務: {}", task.title);
    // task 在函式結束時被丟棄
}

版本 2:返回所有權

fn main() {
    let task = Task {
        title: String::from("學習 Rust 所有權"),
        completed: false,
    };
    
    let task = process_task(task);  // 取回所有權
    
    println!("任務狀態: {:?}", task);  // 現在可以使用了!
}

fn process_task(mut task: Task) -> Task {
    println!("處理任務: {}", task.title);
    task.completed = true;
    task  // 返回所有權
}

版本 3:克隆(Clone)

fn main() {
    let task = Task {
        title: String::from("學習 Rust 所有權"),
        completed: false,
    };
    
    let task_copy = task.clone();  // 明確地複製
    
    process_task(task);
    println!("複製的任務: {:?}", task_copy);  // 可以使用複製品
}

fn process_task(task: Task) {
    println!("處理任務: {}", task.title);
}

所有權檢查器的工作原理

Rust 編譯器會在編譯時追蹤每個值的所有權:

fn main() {
    let s1 = String::from("hello");  // s1 擁有 "hello"
    let s2 = s1;                     // 所有權轉移: s1 -> s2
    let s3 = s2.clone();              // s3 擁有一個複製品
    
    drop(s2);                         // s2 的值被提前釋放
    // println!("{}", s2);            // 錯誤!s2 已經無效
    println!("{}", s3);               // s3 仍然有效
}

常見的所有權模式

1. 建造者模式(Builder Pattern)

struct TaskBuilder {
    title: String,
    description: Option<String>,
}

impl TaskBuilder {
    fn new(title: String) -> Self {
        TaskBuilder {
            title,
            description: None,
        }
    }
    
    fn description(mut self, desc: String) -> Self {
        self.description = Some(desc);
        self  // 返回 self 的所有權
    }
    
    fn build(self) -> Task {
        Task {
            title: self.title,
            completed: false,
        }
    }
}

2. 消耗性方法(Consuming Methods)

impl Task {
    // 這個方法會消耗 self
    fn complete(mut self) -> Self {
        self.completed = true;
        self
    }
    
    // 這個方法不會消耗 self(我們下一章會學到 &self)
    fn is_completed(&self) -> bool {
        self.completed
    }
}

除錯技巧

當遇到所有權錯誤時,編譯器會給出詳細的提示:

error[E0382]: borrow of moved value: `s1`
  --> src/main.rs:5:20
   |
2 |     let s1 = String::from("hello");
   |         -- move occurs because `s1` has type `String`
3 |     let s2 = s1;
   |              -- value moved here
4 |     
5 |     println!("{}", s1);
   |                    ^^ value borrowed here after move

Follow-up

  1. 基礎練習:建立一個函式,接收一個 Vec<String>,返回最長的字串(移動所有權)
fn find_longest(strings: Vec<String>) -> String {
    // 實作這個函式
}
  1. 進階練習:實作一個簡單的堆疊結構,支援 push 和 pop 操作
struct Stack {
    items: Vec<String>,
}

impl Stack {
    fn new() -> Self {
        // 實作
    }
    
    fn push(&mut self, item: String) {
        // 實作
    }
    
    fn pop(&mut self) -> Option<String> {
        // 實作
    }
}

上一篇
Day 7: 第一個完整專案 - 終端機任務管理器
下一篇
Day 9: 借用與參考:不轉移所有權
系列文
30天Rust從零到全端15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言