在 Rust 中,結構體(struct)不僅是組織資料的工具,也常與函式搭配使用來建立與組裝實例。此小節將以 Coffee
結構體為例,從基本定義到函式返回 struct、欄位擴展(..
語法)、擁有權(ownership)等進行逐步教學。
// 1. 將 struct 定義移出 main 函式
struct Coffee {
name: String,
price: i32,
is_hot: bool,
}
fn main() {
// 3. 呼叫函式來創建 Coffee 實例
let coffee = make_coffee(String::from("Latte"), 130, true);
// let coffee: Coffee = ...; // 也可以像這樣明確註釋類型
println!(
"My {} this morning cost {}. It's {} that it was hot.",
coffee.name, coffee.price, coffee.is_hot
);
}
// 2. 定義一個「工廠」函式,專門用來創建並回傳 Coffee 實例
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
Coffee {
name: name,
price: price,
is_hot: is_hot,
}
}
為何要將 struct 定義移出 main?
作用域與重用性:如果 struct Coffee
定義在 main
函式內部,那麼它的作用域就僅限於 main
函式,其他函式(如 make_coffee
)將無法識別 Coffee
這個型別。將其定義在 main
外部(頂層),意味著它在整個模組(通常是整個檔案)中都是可見的,main
和 make_coffee
都可以使用它。
函式的回傳型別 (-> Coffee)
在 make_coffee
函式的簽名中,-> Coffee
這部分明確指定了這個函式執行完畢後,將會回傳一個 Coffee
型別的實例。就像函式可以回傳 String
或 i32
一樣,它完全可以回傳我們自訂的結構體型別。
工廠函式 (Factory Function)make_coffee
函式扮演了一個「工廠」的角色。它的唯一職責就是根據傳入的參數,「製造」出一個設定好的 Coffee
實例並將其回傳。這種模式有助於將實例的創建邏輯封裝起來,讓 main
函式的主流程更清晰。
(筆記補充) 在後續課程中,將會學到關聯函式 (Associated Functions),例如 impl Coffee { fn new(...) -> Coffee { ... } },這是 Rust 中創建結構體實例更為慣用的方法。
現在我們來仔細追蹤一下,當我們呼叫 make_coffee 時,所有權是如何變化的。
struct Coffee {
name: String,
price: i32,
is_hot: bool,
}
fn main() {
// 1. 在 main 中創建一個 String,變數 name 擁有它
let name = String::from("Latte");
// 2. 呼叫 make_coffee,將 name 傳入
let coffee = make_coffee(name, 130, true);
println!(/* ... */);
// 3. 此時若嘗試使用 name,將會報錯,因為所有權已轉移
// println!("{}", name); // 這行會編譯失敗!
}
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
// 4. name 參數現在擁有傳入的 String
Coffee {
name: name, // 5. name 的所有權再次移動到 Coffee 實例的 name 欄位
price: price,
is_hot: is_hot,
}
// 6. Coffee 實例被回傳,其所有權轉移給 main 中的 coffee 變數
}
所有權的旅程:
main
函式中,let name = String::from("Latte");
創建了一個 String,其所有權屬於 main
中的 name
變數。make_coffee(name, ...)
時,因為 String 型別沒有實作 Copy Trait
,所以 name
變數的所有權被移動 (moved) 到 make_coffee
函式的 name 參數。main
函式中的 name
變數立即失效,不能再被使用。make_coffee
函式內部,name
參數成為了這個 String 的新擁有者。Coffee { name: name, ... }
執行時,name 參數的所有權再次移動,這次是移給了新創建的 Coffee
實例的 name
欄位。make_coffee
函式回傳這個 Coffee
實例,其所有權被完整地轉移給了 main
函式中的 coffee
變數。當變數名稱和結構體的欄位名稱完全相同時,Rust 提供了一種方便的簡寫語法,讓程式碼更簡潔。
// ... (Coffee struct 定義)
fn main() {
// ... (第一個 coffee 實例的創建)
// 使用簡寫法創建第二個 coffee 實例
let name = String::from("Caramel Macchiato");
let price = 150;
let is_hot = false;
let coffee = Coffee {
name, // 等同於 name: name,
price, // 等同於 price: price,
is_hot, // 等同於 is_hot: is_hot,
};
println!("{}, {}, {}", coffee.name, coffee.price, coffee.is_hot);
}
// 函式也可以使用簡寫法
fn make_coffee(name: String, price: i32, is_hot: bool) -> Coffee {
Coffee {
name, // 因為參數名稱和欄位名稱相同
price,
is_hot,
}
}
make_coffee
函式和 main
函式裡 Caramel Macchiato
的創建都展示了這個技巧。當我們想創建一個與現有實例大部分欄位都相同,只有少數欄位不同的新實例時,「結構體更新語法」就派上用場了。
// ... (struct 和 make_coffee 函式定義)
fn main() {
let mocha = make_coffee(String::from("Latte"), 130, true);
// 使用 mocha 的部分值來創建 caramel_macchiato
let caramel_macchiato = Coffee {
name: String::from("Caramel Macchiato"),
..mocha // 更新語法
};
println!(
"{}, {}, {}",
caramel_macchiato.name, // "Caramel Macchiato"
caramel_macchiato.price, // 130 (來自 mocha)
caramel_macchiato.is_hot // true (來自 mocha)
);
}
..實例名稱
這個語法必須放在結構體初始化的大括號中的最後。它的意思是:「對於我在前面沒有明確指定的任何欄位,請從 mocha
實例中獲取其對應的值。」caramel_macchiato
是一個全新的、獨立的 Coffee
實例。name
欄位被明確地設定為 String::from("Caramel Macchiato")
。price
和 is_hot
欄位沒有被明確指定,因此它們會從 mocha
實例中取得對應的值。這個過程可能是「複製」(Copy) 或「移動」(Move),取決於欄位的型別。結構體更新語法與所有權系統的互動非常緊密,特別是當欄位型別不具備 Copy Trait 時。
// ... (struct 和 make_coffee 函式定義)
fn main() {
let mocha = make_coffee(String::from("Latte"), 130, true);
let caramel_macchiato = Coffee {
// name 欄位沒有被指定,將會從 mocha 中獲取
..mocha
};
println!("{}", caramel_macchiato.name); // 印出 "Latte"
// 下面這行會導致編譯錯誤!
// println!("{}", mocha.name);
}
..mocha
的所有權轉移:
..mocha
時,Rust 會逐一檢查 mocha 的欄位。mocha.price
(型別 i32) 和 mocha.is_hot
(型別 bool) 都實作了 Copy Trait
,所以它們的值會被複製到 caramel_macchiato
的對應欄位中。mocha.name
(型別 String) 沒有實作 Copy Trait
。因此,它的所有權會被移動到 caramel_macchiato.name
中。mocha.name
的所有權已經轉移給了 caramel_macchiato,mocha
實例本身變成了「部分移動」的狀態。你不能再存取 mocha.name
,因為它已經不屬於 mocha
了。println!("{}", mocha.name);
會報錯的原因。如果我們希望在更新後,原始的 mocha 實例仍然保持完整可用,我們就需要對那些會被「移動」的欄位進行顯式的「複製」(clone)。
// ... (struct 和 make_coffee 函式定義)
fn main() {
let mocha = make_coffee(String::from("Latte"), 130, true);
let caramel_macchiato = Coffee {
// 顯式地複製 name 欄位
name: mocha.name.clone(),
..mocha
};
println!("Caramel Macchiato's name: {}", caramel_macchiato.name);
// 現在這行可以正常運作了!
println!("Mocha's name is still: {}", mocha.name);
}
clone()
的作用:clone()
方法可以為某些型別(如 String)創建一個完整的、深層的副本。這意味著它會在堆上分配一塊新的記憶體,並將原始數據複製過去。name: mocha.name.clone()
:mocha.name
的 String 數據被複製。caramel_macchiato.name
得到了這個全新的、獨立的 String 副本的所有權。mocha
仍然擁有其原始的 name
欄位。..mocha
語法接著執行,但此時 name
欄位已經被指定,所以它只會處理剩下的 price
和 is_hot
。這兩個欄位因為是 Copy 型別,所以會被複製。mocha
的任何欄位的所有權都沒有被移動。因此,在創建 caramel_macchiato
之後,mocha
實例仍然是完全有效的,我們可以繼續存取它的所有欄位,包括 mocha.name
。主題 | 重點 |
---|---|
struct 定義 | 建議放在 main 外,供全域函式使用 |
回傳 struct | 函式可用 -> Coffee 直接返回 struct 實例 |
擁有權 | String 等型別會搬移所有權,需注意欄位使用 |
.. 語法 | 可快速從另一個 struct 實例擴展欄位值 |
clone | 避免所有權轉移的錯誤方式,產生一份新的內容副本 |
透過這些技巧與語法,Rust 的結構體操作會更彈性、更安全,也更能符合所有權與生命週期的編譯期檢查邏輯。