Ch5:特徵與泛型 — 行為與能力的模組化
·
Sean Sie
impl:賦予資料靈魂
Rust 雖然不是傳統那種繼承滿天飛的物件導向語言(OOP),但它透過 impl 塊,讓你能夠幫結構體或列舉定義「方法」。這就像是給一個靜態的資料結構注入了靈魂,讓它從「一堆資料」變成「能做事的物件」。
方法與關聯函數
在 impl 塊裡,函數分為兩類:
- 方法 (Methods):第一個參數是
self(或是&self、&mut self),需要透過執行個體來呼叫。 - 關聯函數 (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 內建的超能力
- Display vs Debug:
- Debug (
{:?}):開發者專用,通常用#[derive(Debug)]自動生成. - Display (
{}):給最終使用者看的,必須手動定義怎麼呈現.
- Debug (
- Add/Sub/Mul:運算子多載. 想要你的結構體支援
+號?實作std::ops::Add就行了. - Deref (解參考):這是 Rust 的魔法. 它讓
String能在需要&str的地方自動轉換. 這叫「Deref 強制轉換(Deref Coercion)」. - Drop:解構子. 當變數離開作用域時要做什麼?(比如關閉檔案、斷開網路連接),都在這裡定義.
- Clone vs Copy:
- Clone:深拷貝(Deep Copy),會有開銷,需要顯式呼叫
.clone(). - Copy:淺拷貝(Shallow Copy),發生在賦值時,只有 Stack 上的簡單型態能實作.
- Clone:深拷貝(Deep Copy),會有開銷,需要顯式呼叫
裝飾性特徵 (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,因為編譯器在那張虛擬表裡放不下這些變動的資訊.