跳轉到

2.1 所有權與借用:線性型別理論的工程實踐#

回顧:為什麼需要所有權#

1.2 節 已說明理論源頭(線性邏輯)。本節把焦點放在 Web 開發中所有權如何影響日常的資料建模決策 —— 這才是它對後端工程師的實際意義。

Move、Copy、Clone 三種語意#

// Copy 型別:棧上小值,賦值自動複製(i32, bool, f64)
let a = 42;
let b = a;
println!("{a}"); // OK,i32 是 Copy

// Move 語意:堆積資源,所有權轉移
let s1 = String::from("user input");
let s2 = s1;          // s1 失效 — 避免了 double free

// Clone:明確深拷貝 — 成本由程式碼中的 .clone() 顯性化
let s3 = s2.clone();

設計洞見:Rust 把「拷貝的成本」變成語法上的顯性標記。C++ 中 = 的深拷貝是隱形的效能炸彈;Rust 中 clone() 一目了然。這是「顯性化成本」原則的體現。

借用:只讀別名與可寫別名#

fn length(s: &String) -> usize { s.len() }   // 借用,不取得所有權
fn push(s: &mut String, c: char) { s.push(c) } // 可變借用

規則(編譯期強制):

\[ \text{任意時刻,對同一值}: \begin{cases} \text{多個 } \&T \quad (\text{共享}) \\ \text{恰好一個 } \&mut T \quad (\text{獨占}) \end{cases} \]

在 Web Handler 中這意味著什麼#

一個 HTTP handler 常常需要「同時讀設定、寫資料庫」。借用規則逼你縮小可變資料的存活範圍:

async fn update_user(
    State(pool): State<PgPool>,          // 克隆的連線池:廉價的 Arc
    Path(id): Path<Uuid>,                // 從 URL 提取
    Json(body): Json<UpdateUser>,        // 從 body 反序列化(取得所有權)
) -> Result<Json<User>, AppError> {
    let user = sqlx::query_as!(User,
        "UPDATE users SET name = $1 WHERE id = $2 RETURNING *",
        body.name, id
    ).fetch_one(&pool).await?;
    Ok(Json(user))
}

注意 body.name 被移動進查詢 —— 之後 body 不可再用。這看似麻煩,實則杜絕了「同一份輸入被用兩次」造成的不一致。

生命週期:引用的有效區間#

// 函數回傳的引用,存活期不得超過輸入
fn first_name<'a>(full: &'a str) -> &'a str {
    full.split(' ').next().unwrap()
}

生命週期標註不改变程式行為,只是給編譯器的證明提示。多數情況編譯器可自動推導(生命周期省略規則),只有歧義時需手寫。

與 GC 語言的心智模型對照#

概念 JavaScript Rust
傳遞物件 傳引用,隨處可改 預設 move,需 & 才共享
修改共用狀態 隨時,可能 race 需 &mut(唯一)或鎖
資源釋放 GC 決定時機 作用域結束確定性釋放(RAII)

RAII(Resource Acquisition Is Initialization)源自 C++,Rust 發揚光大:檔案、鎖、資料庫連線都綁定型別的 drop,忘記釋放在編譯期就不可能。

共享狀態的正確姿勢:Arc + Mutex#

Web 伺服器必然有跨請求共享狀態(連線池、快取)。Rust 的解法是顯式包裝:

use std::sync::{Arc, Mutex};

struct AppState {
    counter: Mutex<u64>,   // 編譯器強制:必須鎖住才能改
}

// counter.lock().unwrap() 回傳守衛(guard),
// 守衛 drop 時自動解鎖 — 忘記解鎖不可能

在 axum 中,State 就是 Arc<AppState> 的語法糖 —— 共享不變的架構狀態用 Arc(原子引用計數,讀取無鎖),可變狀態用 Mutex(或更好的做法:用訊息傳遞,見 2.3 節)。

習題#

  1. 解釋為何 String 不實作 Copy 而 i32 實作。若 String 是 Copy 會出什麼問題?
  2. 以下程式碼為何編譯失敗?兩種修法?
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("{first}");
    
  3. 為什麼說 RAII 讓「忘記關連線」成為不可能?與 try...finally 模式比較。