在 Rust 中,函式參數的傳遞方式與擁有權、借用(borrowing)息息相關,尤其當傳遞的是 struct
這樣的複合型別時。此外,Rust 也為輸出資料提供了強大的格式化工具,其中最常見的便是 Display
與 Debug
。本篇將探討函式與結構體互動的幾種方式,以及如何讓我們自訂的結構體能夠方便地顯示出來。
函式在接收結構體實例時,可以根據需求採用不同的方式,主要影響到所有權以及是否能修改結構體內容。這四種主要方式是:
其中 drink_something 函式使用了「可變借用」:
struct Coffee {
name: String,
price: i32,
is_hot: bool,
}
fn main() {
// 1. 創建一個可變的 Coffee 實例 mocha
let mut mocha = make_coffee(String::from("Latte"), 130, true);
// 2. 將 mocha 的可變引用傳遞給 drink_something 函式
drink_something(&mut mocha);
// 3. mocha 的 price 欄位已被 drink_something 函式修改
println!("After drinking, {} costs {}, still hot? We didn't track that change.", mocha.name, mocha.price);
// (以下為後續討論 Display 和 Debug 的範例,暫與此主題無關)
let value = ["Hello,", "World!"];
// println!("{}", value); // 這行直接用 {} 印陣列會報錯,因為陣列沒有直接實作 Display
println!("Array with Debug: {:?}", value);
println!("Array with Pretty Debug: {:#?}", value);
}
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
Coffee {
name,
price,
is_hot,
}
}
// drink_something 函式接收一個對 Coffee 實例的可變引用
fn drink_something(coffee: &mut Coffee) {
coffee.price = 200; // 透過可變引用修改 coffee 的 price 欄位
println!("Inside drink_something: {}, price changed to {}", coffee.name, coffee.price);
}
let mut mocha = ...
:在 main
函式中,我們創建了 mocha
。注意這裡使用了 mut
關鍵字,因為我們稍後需要取得它的一個可變引用來修改它。drink_something(&mut mocha)
:
mocha
的一個可變引用 (&mut Coffee)
傳遞給 drink_something
函式。main
函式仍然保有 mocha
的所有權,但暫時將「修改權限」借給了 drink_something
函式。drink_something
的生命週期內,main
函式不能再對 mocha
進行其他可變或不可變的借用(這是 Rust 的借用規則,防止數據競爭)。fn drink_something(coffee: &mut Coffee)
:
coffee
的參數,其型別是 &mut Coffee
。coffee.price = 200;
這行程式碼透過這個可變引用,成功地修改了原始 mocha
實例的 price 欄位的值。drink_something
函式執行完畢後,控制權回到 main
函式。mocha
的 price 欄位的值已經變成了 200。drink_something
的簽名是 fn drink_something(c: Coffee)
,那麼 mocha
的所有權會被移動到函式中。除非函式再把 c
回傳,否則 main
中的 mocha
將不再可用。(fn func(c: &Coffee))
:如果簽名是 fn view_coffee(c: &Coffee)
,則函式只能讀取 c
的欄位,不能修改它們。(fn func(mut c: Coffee))
:如果簽名是 fn consume_and_modify(mut c: Coffee)
,mocha
所有權被移動,且在函式內部 c
是可變的。任何修改只影響這個函式內部的 c
,除非 c
被回傳。選擇哪種方式取決於函式需要對數據做什麼操作:是需要獲取所有權並可能消耗它,還是僅僅讀取它,或是需要修改原始數據。
當我們嘗試印出一個陣列或自訂的結構體時,可能會遇到問題。Rust 的 println! 巨集需要知道如何將一個型別轉換成字串來顯示。這是透過 Display 和 Debug 這兩個 Trait (特徵) 來實現的。
背景知識 (相關概念在此前有討論,這裡再度提起):
let value = ["Hello,", "World!"];
println!("{}", value); // ❌ 錯誤,無 Display trait
println!("{:?}", value); // ✅ Debug 輸出
println!("{:#?}", value); // ✅ Debug,格式化輸出
Rust 中,並非所有型別都能直接透過 {}
印出。若要使用 println!("{}")
,該型別必須實作 Display
trait。陣列 value
就沒有實作 Display,因此會報錯。
若改用 {:?}
或 {:#?}
,則是使用 Debug 格式輸出。這要求型別需實作 Debug
trait。Rust 提供 #[derive(Debug)]
屬性來自動為 struct 實作 Debug:
// 1. 使用 derive 屬性自動實作 Debug Trait
#[derive(Debug)]
struct Coffee {
name: String,
price: i32,
is_hot: bool,
}
fn main() {
let mut mocha = make_coffee(String::from("Latte"), 130, true);
drink_something(&mut mocha);
// println!("{}, {}", mocha.name, mocha.price); // 之前的印出方式,分別印出欄位
// 2. 嘗試印出整個 mocha 實例
// println!("{}", mocha); // 這行仍然會報錯,因為我們沒有手動實作 Display
println!("Mocha (Debug): {:?}", mocha);
println!("Mocha (Pretty Debug): {:#?}", mocha);
}
// make_coffee 和 drink_something 函式定義同上
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {....}
fn drink_something(coffee: &mut Coffee) {....}
#[derive(Debug)]
derive
是一個特殊的屬性,它告訴編譯器根據一些預設的規則自動為我們的結構體(或枚舉)產生某些 Trait 的實作程式碼。#[derive(Debug)]
指示編譯器自動為Coffee
結構體實作 Debug Trait。這意味著Coffee
型別的實例現在可以透過 {:?} 或 {:#?} 格式化符來印出了。mocha
實例:println!("{}", mocha);
#[derive(Debug)]
只為我們實作了 Debug Trait,並沒有實作 Display Trait。{} 格式化符要求型別實作 Display。若要讓此行運作,我們需要手動為Coffee
實作std::fmt::Display Trait
,定義它應該如何被「美觀地」顯示給使用者。主題 | 重點 |
---|---|
Struct 傳參方式 | 可傳所有權、不可變參照、可變參照,依需求決定 |
可變參照 &mut |
可在不奪走所有權下,修改 Struct 欄位 |
Display vs Debug | {} 需實作 Display,{:?} 則需實作 Debug |
Debug 實作 | 使用 #[derive(Debug)] 為 struct 自動加上 Debug 支援 |