本文 AI 產出,尚未審核

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),常用於建構子(如 newdefault)或其他與實例無關的工具函式。

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(...),保持一致性。

最佳實踐

  1. &self 為預設:除非真的需要所有權或可變借用,否則所有方法都應該接受不可變借用。
  2. 將建構子放在單獨的 impl:讓 newdefaultwith_* 等關聯函式集中管理。
  3. 利用 Trait 抽象行為:若多個型別共享相同操作,使用 Trait 而非重複實作。
  4. 保持 impl 小而專注:每個 impl 只負責一組相關功能(例如:基本操作、序列化、驗證),有助於程式碼閱讀與測試。
  5. 使用 #[must_use]:對於返回新值的消費方法(如 addmove_by),加上 #[must_use] 防止忘記使用回傳值。

實際應用場景

場景 為何使用方法與 impl 範例
圖形引擎 讓每個圖形物件(SpriteMesh)自行管理位置、旋轉、縮放等行為。 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) -> u8write(&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 的魅力!