seansie's blog

Ch5:特徵與泛型 — 行為與能力的模組化

· Sean Sie

impl:賦予資料靈魂

Rust 雖然不是傳統那種繼承滿天飛的物件導向語言(OOP),但它透過 impl 塊,讓你能夠幫結構體或列舉定義「方法」。這就像是給一個靜態的資料結構注入了靈魂,讓它從「一堆資料」變成「能做事的物件」。

方法與關聯函數

impl 塊裡,函數分為兩類:

  1. 方法 (Methods):第一個參數是 self(或是 &self&mut self),需要透過執行個體來呼叫。
  2. 關聯函數 (Associated Functions):沒有 self 參數。最常見的就是 new(),類似其他語言的「靜態方法」或「建構子」。
struct Rectangle {  
    width: u32,  
    height: u32,  
}

impl Rectangle {  
    // 關聯函數:用來創建物件  
    fn new(width: u32, height: u32) -> Self {  
        Self { width, height }  
    }

    // 方法:計算面積。&self 代表「唯讀借用」  
    fn area(&self) -> u32 {  
        self.width * self.height  
    }

    // 方法:修改大小。&mut self 代表「可變借用」  
    fn scale(&mut self, factor: u32) {  
        self.width *= factor;  
        self.height *= factor;  
    }  
}

fn main() {  
    let mut rect = Rectangle::new(10, 20); // 呼叫關聯函數  
    println!("面積:{}", rect.area());    // 呼叫方法  
    rect.scale(2);                        // 修改物件  
}
  • ⚡ 極速講解
    • &self:最常用,我只是想「看看」這個物件,不想改它,也不想把它弄壞(轉移所有權)。
    • &mut self:我想改這個物件。
    • self:最少用,呼叫完這個方法後,物件的所有權就被「吃掉」了。通常用於「狀態轉換」(例如把一個「草稿」轉換成「發布文章」)。
  • 常用模式鍊式調用(Chain Calling / Builder Pattern)。讓方法最後回傳 Self,你就能寫出像 rect.resize(100, 100).color("blue").draw() 這種極具藝術感的漂亮代碼。

泛型 (Generics):寫一份代碼,跑遍所有型態

不想為 i32 寫一遍 add,又為 f64 寫一遍一模一樣的 add?泛型 T 是你的救星。

struct Point<T> {  
    x: T,  
    y: T,  
}

impl<T> Point<T> {  
    fn x(&self) -> &T {  
        &self.x  
    }  
}

零成本抽象的祕密:單態化 (Monomorphization)

Rust 的泛型為什麼效能頂級?因為編譯器在編譯時會玩「複製貼上」。如果你用了 Point<i32>Point<f64>,編譯器會幫你生成兩份專屬的機器碼.

  • 優點:執行速度跟手寫特定型別一模一樣,沒有任何執行時期的開銷.
  • 缺點:如果泛型用得太濫,編譯出的執行檔體積會變大(二進制膨脹).

特徵 (Trait):定義能力的介面

特徵就像是「證書」。只要你實作了某個特徵,你就向編譯器保證了你擁有了某種「能力」。

trait Speak {  
    fn say(&self) -> String;

    // 預設實作:如果你不寫,預設就是這個  
    fn introduce(&self) -> String {  
        format!("大家好,我說:{}", self.say())  
    }  
}

這在開發時非常強大,因為你可以針對「行為」而非「型別」來寫函數. 只要會動、會叫的東西,都能傳進來處理.

泛型約束 (Trait Bounds)

如果你寫 fn process<T>(item: T),編譯器會報錯,因為它不知道 T 到底能不能印、能不能加. 這時候你需要「約束」:

use std::fmt::Display;

// 告訴編譯器:T 必須要有「會列印」的能力  
fn print_it<T: Display>(item: T) {  
    println!("{}", item);  
}

// 多重約束:T 必須能印出來,且必須能複製  
fn complex_process<T: Display + Clone>(item: T) {  
    let copy = item.clone();  
    println!("複製品:{}", copy);  
}

讓代碼更優雅:where 語句

當約束太多時,函數簽名會變得像天書. 這時候請出 where

// 亂成一團的寫法  
fn some_fn<T: Clone + Display, U: Debug + Add>(t: T, u: U) { ... }

// 優雅的寫法  
fn some_fn<T, U>(t: T, u: U)  
where  
    T: Clone + Display,  
    U: Debug + Add,  
{  
    // ...  
}

系統核心特徵:Rust 內建的超能力

  1. Display vs Debug
    • Debug ({:?}):開發者專用,通常用 #[derive(Debug)] 自動生成.
    • Display ({}):給最終使用者看的,必須手動定義怎麼呈現.
  2. Add/Sub/Mul:運算子多載. 想要你的結構體支援 + 號?實作 std::ops::Add 就行了.
  3. Deref (解參考):這是 Rust 的魔法. 它讓 String 能在需要 &str 的地方自動轉換. 這叫「Deref 強制轉換(Deref Coercion)」.
  4. Drop:解構子. 當變數離開作用域時要做什麼?(比如關閉檔案、斷開網路連接),都在這裡定義.
  5. Clone vs Copy
    • Clone:深拷貝(Deep Copy),會有開銷,需要顯式呼叫 .clone() .
    • Copy:淺拷貝(Shallow Copy),發生在賦值時,只有 Stack 上的簡單型態能實作.

裝飾性特徵 (Marker Traits):編譯器的標籤

有些特徵根本沒有方法,它們只是「標記」,是用來跟編譯器溝通的:

  • Sized:標記型別的大小在編譯期是否已知(預設所有泛型都是 Sized )。
  • Sync:標記此型別可以在執行緒間安全地「共享」借用(&T)。
  • Send:標記此型別的所有權可以在執行緒間安全地「傳遞」。

這些標記就像是 VIP 會員卡. 如果你的資料沒有 Send 標籤,你想把它傳到另一個執行緒?編譯器會直接把你攔下來.

dyn (Dynamic Dispatch):動態分派

這是在處理「異質清單」時的終極大招. 例如你有一堆不同的動物,它們都實作了 Speak 特徵,你想把它們塞進同一個 Vec

let animals: Vec<Box<dyn Speak>> = vec![  
    Box::new(Dog {}),  
    Box::new(Cat {}),  
];

for animal in animals {  
    println!("{}", animal.say());  
}

靜態分派 vs 動態分派

特性 泛型 (靜態分派) dyn (動態分派)
運作時間 編譯期確定 執行期確定
效能 極快(可內聯優化) 較慢(需查虛擬表 VTable)
代碼體積 可能膨脹(多份拷貝) 較小(一份代碼)
靈活性 同一個 Vec 只能放同一型別 同一個 Vec 可以放不同型別

原則:優先用泛型(靜態分發),只有在「真的沒辦法確定型態」時才用 dyn(動態分發). 記住, dyn 的物件必須是物件安全的:不能有泛型方法,也不能回傳 Self,因為編譯器在那張虛擬表裡放不下這些變動的資訊.