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 時自動解鎖 — 忘記解鎖不可能
flowchart TD
A[需要跨執行緒共享] --> B{需要修改?}
B -->|唯讀| C[Arc<T>]
B -->|寫入| D[Arc<Mutex<T>>]
D --> E[lock() 取得守衛]
E --> F[守衛 drop 自動解鎖]在 axum 中,State 就是 Arc<AppState> 的語法糖 —— 共享不變的架構狀態用 Arc(原子引用計數,讀取無鎖),可變狀態用 Mutex(或更好的做法:用訊息傳遞,見 2.3 節)。
習題#
- 解釋為何
String不實作Copy而i32實作。若String是 Copy 會出什麼問題? - 以下程式碼為何編譯失敗?兩種修法?
let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); println!("{first}"); - 為什麼說 RAII 讓「忘記關連線」成為不可能?與
try...finally模式比較。