Rust 課程 – 結構體與方法
方法(Methods)與 impl 塊
簡介
在 Rust 中,**方法(method)**是與特定資料型別(通常是結構體或列舉)緊密結合的函式。與一般的自由函式不同,方法必須在 impl(implementation)塊內宣告,這讓程式碼的組織與可讀性大幅提升。
掌握方法與 impl 塊的使用,不僅能讓你寫出符合所有權與借用規則的安全程式,還能在大型專案中建立清晰的抽象層,讓結構體的行為與資料本身緊密結合,符合 面向物件 的設計思維。
本篇文章將從核心概念說明、實作範例、常見陷阱與最佳實踐,直到實務應用場景,完整帶你一步步熟悉 Rust 方法的寫法與使用方式。
核心概念
1. impl 塊的基本語法
impl 用來為一個型別(struct、enum、trait)提供實作。最簡單的形式如下:
struct Point {
x: f64,
y: f64,
}
impl Point {
// 這是一個關聯函式(不需要 self)
fn origin() -> Self {
Self { x: 0.0, y: 0.0 }
}
// 這是一個方法(必須有 self 參數)
fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
Self代表目前impl所屬的型別(此例為Point)。- 方法的第一個參數必須是
self、&self、或&mut self,分別代表「取得所有權」「借用」或「可變借用」的行為。 - 沒有
self參數的函式稱為 關聯函式(associated function),常用於建構子(如new、default)或其他與實例無關的工具函式。
2. 方法的接收者類型
| 接收者 | 語意 | 範例 |
|---|---|---|
self |
取得 所有權,呼叫後原變數無法再使用 | fn consume(self) {} |
&self |
不可變借用,允許多個同時讀取 | fn len(&self) -> usize {} |
&mut self |
可變借用,一次只能有一個可變參考 | fn push(&mut self, v: T) {} |
注意:Rust 的借用檢查器會在編譯期保證不會同時出現多個可變參考或可變/不可變衝突,這是安全性的核心。
3. 多重 impl 塊與泛型
同一個型別可以有多個 impl 塊,甚至可以針對不同的泛型參數提供不同的實作:
struct Wrapper<T> {
value: T,
}
// 為所有 T 實作一個通用方法
impl<T> Wrapper<T> {
fn new(v: T) -> Self {
Self { value: v }
}
fn inner(&self) -> &T {
&self.value
}
}
// 為特定的 T = i32 實作額外方法
impl Wrapper<i32> {
fn double(&mut self) {
self.value *= 2;
}
}
4. 伴隨函式(Associated Functions)與 Self:: 呼叫
在 impl 內的關聯函式,通常會以 Self:: 來呼叫其他關聯函式或常數:
impl Point {
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
fn origin() -> Self {
Self::new(0.0, 0.0)
}
}
5. 方法鏈(Method Chaining)與所有權傳遞
若方法回傳 Self(或 Self 的所有權),可以形成 方法鏈:
impl Point {
fn move_by(mut self, dx: f64, dy: f64) -> Self {
self.x += dx;
self.y += dy;
self
}
}
// 使用鏈式呼叫
let p = Point::origin().move_by(3.0, 4.0);
程式碼範例
以下提供 4 個實用範例,說明不同情境下方法的寫法與注意事項。
範例 1:簡易向量(Vector)結構與基本運算
/// 二維向量
#[derive(Debug, Clone, Copy)]
struct Vec2 {
x: f64,
y: f64,
}
impl Vec2 {
/// 建構子
fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
/// 計算向量長度(不可變借用)
fn magnitude(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
/// 向量相加,回傳新的 Vec2(取得所有權)
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
// 使用範例
let v1 = Vec2::new(3.0, 4.0);
println!("v1 = {:?}, |v1| = {}", v1, v1.magnitude());
let v2 = Vec2::new(1.0, 2.0);
let v3 = v1.add(v2); // v1 已被搬走,無法再使用
println!("v3 = {:?}", v3);
重點:
add取得self的所有權,讓編譯器知道v1在呼叫後不再有效,避免不必要的複製。
範例 2:可變借用與內部可變性(Interior Mutability)
use std::cell::RefCell;
/// 簡易計數器,使用 RefCell 允許在不可變參考下修改值
struct Counter {
count: RefCell<i32>,
}
impl Counter {
fn new() -> Self {
Self {
count: RefCell::new(0),
}
}
/// 增加計數(只需要 &self)
fn inc(&self) {
*self.count.borrow_mut() += 1;
}
fn get(&self) -> i32 {
*self.count.borrow()
}
}
// 使用範例
let c = Counter::new();
c.inc();
c.inc();
println!("counter = {}", c.get()); // 仍是不可變參考 c
說明:
RefCell讓我們在 不可變借用 (&self) 時仍能修改內部資料,這在需要共享可變狀態的情境(如 GUI、事件系統)非常實用。但要小心 執行時 的借用檢查錯誤。
範例 3:多重 impl 塊與 Trait 實作
/// 圓形
struct Circle {
radius: f64,
}
// 只提供基礎建構子
impl Circle {
fn new(r: f64) -> Self {
Self { radius: r }
}
}
// 為 Circle 實作面積計算的 Trait
trait Area {
fn area(&self) -> f64;
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
// 使用範例
let c = Circle::new(2.5);
println!("area = {}", c.area()); // 直接呼叫 trait 方法
要點:
impl可以分散在不同的檔案或模組,讓每個功能(建構子、Trait 實作)保持獨立,提升可維護性。
範例 4:方法鏈與所有權轉移(Builder Pattern)
#[derive(Debug)]
struct HttpRequest {
method: String,
url: String,
body: Option<String>,
}
impl HttpRequest {
fn new(method: &str, url: &str) -> Self {
Self {
method: method.to_string(),
url: url.to_string(),
body: None,
}
}
fn body(mut self, b: &str) -> Self {
self.body = Some(b.to_string());
self
}
fn send(self) {
// 此處僅示意,實際上會呼叫 reqwest 等套件
println!("Sending {:?} request to {}", self.method, self.url);
if let Some(b) = self.body {
println!("Body: {}", b);
}
}
}
// 鏈式呼叫
HttpRequest::new("POST", "https://example.com/api")
.body("{\"name\":\"Rust\"}")
.send();
技巧:在 Builder Pattern 中,方法通常回傳
Self(所有權),使得每一步都能「消費」前一步的值,最終只剩下一個完整的物件。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記 &self |
方法忘記加 &,導致編譯錯誤或不必要的所有權搬移。 |
檢查是否真的需要取得所有權;大多數只讀方法應使用 &self。 |
過度使用 self |
把本該只讀的函式寫成 self,造成呼叫者必須先 clone。 |
盡量使用 &self 或 &mut self,只有在需要消費自身時才使用 self。 |
| RefCell 產生執行時 panic | 在同一時間內多次 borrow_mut() 會 panic。 |
確保借用範圍最小化,或改用 Mutex/RwLock(多執行緒情境)。 |
多個 impl 塊的衝突 |
同一型別在不同 impl 中定義相同名稱的方法會衝突。 |
統一管理,或使用不同的 Trait 來分離功能。 |
忘記 Self:: 呼叫關聯函式 |
在同一 impl 中直接呼叫 new 會被視為自由函式。 |
使用 Self::new(...),保持一致性。 |
最佳實踐
- 以
&self為預設:除非真的需要所有權或可變借用,否則所有方法都應該接受不可變借用。 - 將建構子放在單獨的
impl:讓new、default、with_*等關聯函式集中管理。 - 利用 Trait 抽象行為:若多個型別共享相同操作,使用 Trait 而非重複實作。
- 保持
impl小而專注:每個impl只負責一組相關功能(例如:基本操作、序列化、驗證),有助於程式碼閱讀與測試。 - 使用
#[must_use]:對於返回新值的消費方法(如add、move_by),加上#[must_use]防止忘記使用回傳值。
實際應用場景
| 場景 | 為何使用方法與 impl |
範例 |
|---|---|---|
| 圖形引擎 | 讓每個圖形物件(Sprite、Mesh)自行管理位置、旋轉、縮放等行為。 |
sprite.translate(dx, dy)、mesh.rotate(angle) |
| 網路客戶端 | 使用 Builder Pattern 組裝 HTTP 請求,確保不可變的配置在建構完成前不被改變。 | HttpRequest::new(...).header(...).body(...).send(); |
| 資料庫模型 | 為 ORM 結構提供 CRUD 方法,封裝底層 SQL 執行細節。 | User::find(id), user.save() |
| 遊戲狀態機 | 以 enum State 搭配 impl State 實作 update(&mut self, ctx: &mut Context),讓每個狀態自行處理邏輯。 |
state.update(&mut ctx); |
| 嵌入式驅動 | 為硬體抽象層(HAL)提供 read(&self) -> u8、write(&mut self, v: u8) 等方法,保證安全的所有權與借用。 |
i2c.read(addr)、spi.write(data) |
在上述情境中,方法不僅提升程式碼的可讀性,還能讓編譯器在所有權、生命週期上提供更嚴格的保證,減少執行時錯誤。
總結
impl塊是 為型別提供行為 的核心機制,方法必須在其中宣告。- 方法的接收者 (
self、&self、&mut self) 決定了所有權與可變性的語意,正確選擇能避免不必要的搬移或借用衝突。 - 透過 多重
impl、泛型、Trait,我們可以把功能模組化、抽象化,讓程式碼更易維護。 - 常見的陷阱包括忘記
&、過度使用所有權、RefCell的執行時 panic 等,遵守最佳實踐(以&self為預設、分離建構子、使用 Trait)可大幅降低錯誤機率。 - 在實務開發(圖形、網路、資料庫、嵌入式)中,方法與
impl為 封裝行為、保證安全 提供了強大的工具。
掌握了方法與 impl 的寫法,你就能在 Rust 中建立既安全又具可讀性的抽象層,為日後開發大型系統奠定堅實基礎。祝你寫程式快樂,持續探索 Rust 的魅力!