Rust 課程 – 結構體與方法
主題:結構體的所有權與借用
簡介
在 Rust 中,所有權(ownership) 與 借用(borrowing) 是語言安全性的核心機制。它們不僅保證了記憶體安全,還讓編譯器在編譯階段就能偵測出許多常見的錯誤。當我們把資料封裝進 結構體(struct) 時,所有權的規則仍然適用,而結構體本身也會成為借用的單位。
如果不了解結構體與所有權、借用之間的互動,就很容易在撰寫 API、實作方法或傳遞資料時碰到「值已被移動」或「不可變參照與可變參照同時存在」的編譯錯誤。本文將從概念說明、實作範例、常見陷阱到實務應用,完整闡述 結構體的所有權與借用,協助初學者與中階開發者在寫 Rust 程式時更得心應手。
核心概念
1️⃣ 結構體的所有權規則
| 規則 | 說明 |
|---|---|
| 每個值都有唯一的所有者 | 結構體本身是一個值,當它被賦值或傳遞時,所有權會被 移動(move) 或 複製(copy)(若類型實作 Copy)。 |
| 所有權只能有一個活躍的持有者 | 移動後,原本的變數將不再可用。 |
| 當所有者離開作用域時,資源會被釋放 | 結構體的 Drop 實作會在作用域結束時自動呼叫。 |
範例 1:結構體所有權的移動
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 10, y: 20 };
// p1 的所有權被移動到 p2
let p2 = p1; // ← p1 失效,若再使用會編譯錯誤
// println!("{:?}", p1); // 編譯錯誤:value used after move
println!("p2: ({}, {})", p2.x, p2.y);
}
重點:
Point沒有實作Copy,所以賦值會觸發 所有權移動。
2️⃣ 借用結構體:不可變與可變參照
Rust 允許我們 借用 結構體的所有權,而不必真的把它移走。借用分為兩種:
&T:不可變參照,同時可以有多個。&mut T:可變參照,同一時間只能有一個,且不能與任何不可變參照同時存在。
範例 2:不可變借用
fn print_point(p: &Point) {
// 只能讀取,不能修改
println!("x = {}, y = {}", p.x, p.y);
}
fn main() {
let p = Point { x: 5, y: 8 };
print_point(&p); // 借用 p 的不可變參照
// 此時仍可再次借用或直接使用 p
println!("still can use p: ({}, {})", p.x, p.y);
}
範例 3:可變借用
fn move_point(p: &mut Point, dx: i32, dy: i32) {
p.x += dx;
p.y += dy;
}
fn main() {
let mut p = Point { x: 0, y: 0 };
move_point(&mut p, 3, 4); // 可變借用
println!("moved: ({}, {})", p.x, p.y);
}
注意:
move_point必須接受&mut Point,因為它要改變結構體的內部欄位。
3️⃣ 方法接收者 (self) 的所有權與借用
在 impl 區塊裡,我們可以為結構體定義三種常見的 方法接收者:
| 接收者 | 語意 | 何時使用 |
|---|---|---|
self |
取得所有權,方法結束後值會被移走或被回傳。 | 需要消耗結構體、或回傳自身(例如 into_inner)。 |
&self |
不可變借用,只能讀取。 | 只需要讀取資料,且允許同時多個呼叫。 |
&mut self |
可變借用,允許修改。 | 需要改變結構體內部狀態。 |
範例 4:三種接收者的比較
impl Point {
// 取得所有權,回傳自身的 x 座標
fn into_x(self) -> i32 {
self.x // self 被消費,方法結束後 p 不再可用
}
// 不可變借用,只讀取
fn distance_from_origin(&self) -> f64 {
((self.x.pow(2) + self.y.pow(2)) as f64).sqrt()
}
// 可變借用,改變座標
fn translate(&mut self, dx: i32, dy: i32) {
self.x += dx;
self.y += dy;
}
}
fn main() {
let p = Point { x: 3, y: 4 };
let d = p.distance_from_origin(); // &self
println!("distance = {}", d);
let mut p2 = Point { x: 1, y: 2 };
p2.translate(5, 5); // &mut self
println!("translated = ({}, {})", p2.x, p2.y);
let x = p2.into_x(); // self(所有權被消費)
// println!("{:?}", p2); // 編譯錯誤:value used after move
println!("x = {}", x);
}
4️⃣ 結構體裡的引用與生命週期(Lifetime)
當結構體的欄位本身是引用時,生命週期 必須明確標註,否則編譯器無法保證引用在結構體使用期間仍然有效。
struct RefPoint<'a> {
x: &'a i32,
y: &'a i32,
}
fn make_ref_point<'a>(a: &'a i32, b: &'a i32) -> RefPoint<'a> {
RefPoint { x: a, y: b }
}
fn main() {
let a = 10;
let b = 20;
let rp = make_ref_point(&a, &b);
println!("ref point: ({}, {})", rp.x, rp.y);
}
技巧:若結構體只在短暫的作用域內使用,盡量避免 把引用放進結構體;改用擁有所有權的類型(如
String、Vec<T>)會讓程式更容易管理。
5️⃣ 常見的所有權/借用組合
| 情境 | 正確寫法 | 常見錯誤 |
|---|---|---|
| 將結構體傳入只讀函式 | fn foo(p: &MyStruct) { … } 並呼叫 foo(&s); |
直接傳 s(移動)導致後續無法使用 |
在同一作用域同時持有 &mut 與 & |
先結束可變借用(作用域結束)再建立不可變借用 | 同時持有 &mut s 與 &s 編譯錯誤 |
| 返回引用 | 必須使用生命週期參數,或返回擁有所有權的值 | 返回局部變數的引用導致編譯錯誤 |
常見陷阱與最佳實踐
🚩 陷阱 1:不小心移動結構體
let a = Point { x: 1, y: 2 };
let b = a; // a 已被移動
println!("{:?}", a); // ❌ 編譯錯誤
解法:若只想「共享」而不消耗,使用借用 &a 或 &mut a;若需要多個擁有者,可考慮 Rc<T>(單執行緒)或 Arc<T>(多執行緒)。
🚩 陷阱 2:同時持有可變與不可變參照
let mut p = Point { x: 0, y: 0 };
let r1 = &p; // 不可變借用
let r2 = &mut p; // ❌ 同時持有可變借用
最佳實踐:縮小借用的作用域,或使用 RefCell<T> 於單執行緒環境下提供「執行時」的可變借用檢查。
🚩 陷阱 3:在結構體中存放引用卻忘記生命週期
struct Bad<'a> { data: &'a str }
fn create() -> Bad<'static> { // 錯誤的生命週期標註
let s = String::from("hello");
Bad { data: &s } // s 會在此函式結束後被釋放
}
解法:避免在需要長期保存的結構體裡使用引用;改用 String、Vec<T> 等擁有所有權的型別,或明確傳入外部生命週期較長的資料。
✅ 最佳實踐總結
| 建議 | 為什麼 |
|---|---|
盡量讓結構體擁有自己的資料(String、Vec<T>) |
減少生命週期管理的複雜度 |
使用 &self 作為只讀方法的接收者 |
允許同時多個呼叫,提高併發性 |
使用 &mut self 只在必要時 |
防止不必要的可變借用衝突 |
對於需要共享所有權的情境,使用 Rc<T> / Arc<T> |
避免所有權移動導致的「使用後已被移動」錯誤 |
在需要在執行時改變借用規則時,考慮 RefCell<T> / Mutex<T> |
提供內部可變性,同時保留編譯期安全檢查 |
實際應用場景
1️⃣ 資料模型與序列化
在 Web 服務或 CLI 工具中,我們常把資料模型(struct)序列化成 JSON、MessagePack 等格式。序列化函式通常接受 &self,因為它只需要讀取資料:
use serde::Serialize;
use serde_json;
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
impl User {
fn to_json(&self) -> String {
serde_json::to_string(self).unwrap()
}
}
2️⃣ 互動式圖形介面(GUI)中的狀態管理
在即時渲染或遊戲開發中,畫面狀態 常以結構體保存。渲染函式會取得 &self,而更新函式則需要 &mut self:
struct GameState {
player_x: f32,
player_y: f32,
score: u32,
}
impl GameState {
fn render(&self) {
// 只讀取狀態,繪製畫面
}
fn update(&mut self, dt: f32) {
// 根據時間 dt 改變位置
self.player_x += dt * 5.0;
}
}
3️⃣ 多執行緒任務佇列
使用 Arc<Mutex<T>> 包裝共享的結構體,讓多個執行緒可以安全地 取得可變借用:
use std::sync::{Arc, Mutex};
use std::thread;
#[derive(Debug)]
struct Counter {
value: usize,
}
fn main() {
let counter = Arc::new(Mutex::new(Counter { value: 0 }));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut guard = c.lock().unwrap(); // 取得可變借用
guard.value += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("final count = {}", counter.lock().unwrap().value);
}
總結
- 所有權與借用是 Rust 保證記憶體安全的根本,結構體不例外。
- 了解
self、&self、&mut self三種方法接收者的差異,能幫助我們設計出 清晰、可預測 的 API。 - 當結構體內部包含 引用 時,必須正確標註 生命週期,或盡量使用擁有所有權的型別以降低複雜度。
- 常見的錯誤包括 所有權意外移動、同時持有可變與不可變借用、以及 錯誤的生命週期標註,只要遵守 「借用要麼不可變,要麼唯一可變」 的原則,就能避免大多數編譯錯誤。
- 在實務開發中,資料模型、即時渲染、跨執行緒共享 等情境都會頻繁碰到結構體的所有權與借用問題,熟練本章內容將直接提升程式的可讀性、效能與安全性。
**掌握結構體的所有權與借用,是成為 Rust 高手的必經之路。**只要在設計時先思考「誰擁有這筆資料?」、「何時需要可變或不可變的存取?」,就能寫出既安全又高效的程式碼。祝你在 Rust 的旅程中玩得開心,寫出更多可靠的程式!