結構體 (Struct) 是一個用於存放相關數據的容器。是一個自訂的資料型別,它允許我們將多個相關的數據片段組合在一起,並為這個組合賦予一個有意義的名稱。想像一下,如果你想描述一杯咖啡,你需要多個資訊:它的名字、價格、是否為熱飲等等。將這些零散的數據打包成一個名為 Coffee 的單一整體,會讓程式碼更有組織性、更易於理解。
struct Coffee {
price: i32,
name: String,
is_hot: bool,
}
struct
關鍵字:我們使用 struct
關鍵字來開始定義一個結構體。Coffee
。按照慣例,結構體的名稱使用「大駝峰式命名法」(PascalCase/CamelCase)。{}
:結構體的欄位被包含在一對大括號中。Coffee
這個結構體後,它就成為了我們程式中的一個新的、自訂的資料型別。類型 | 範例格式 | 說明 |
---|---|---|
Named Field Struct | struct Coffee { name: String } |
最常用,有欄位名稱 |
Tuple Struct | struct Color(i32, i32, i32); |
沒有欄位名,像 tuple 的結構體 |
Unit Struct | struct Marker; |
沒有資料,用於型別標記或 trait 實作 |
一旦我們定義了 Coffee 結構體,就可以像下面這樣創建它的實例:
let mocha = Coffee {
price: 150,
name: String::from("Mocha"),
is_hot: true,
};
定義一個 struct 就像是設計一張藍圖。為了實際使用這張藍圖,我們需要根據它創建一個具體的物件。這個根據型別創建出來的具體值,就稱為「實例 (Instance)」
{}
並且為所有欄位提供初始值。struct
實例是不可變的,除非用 mut
宣告println!(
"My {} this morning cost ${}, It's {} that it was hot.",
mocha.name, mocha.price, mocha.is_hot
);
要讀取或存取結構體實例中欄位的值,我們使用「點號標記法」(dot notation),格式為 實例變數.欄位名稱
String "Mocha"
。i32
值 150。bool
值 true
。println!
巨集只是「借用」(borrow) 了這些值來顯示,所以 mocha 實例的所有權沒有發生任何變化,它仍然完整地擁有其所有數據。現在,讓我們來看範例中引發所有權問題的部分:
let new_coffee = mocha.name;
println!("{}", new_coffee);
println!("{}", mocha.name); // 這裡會報錯 ❌
let new_coffee = mocha.name;
mocha.name
欄位的型別是 String。String 型別的數據儲存在堆積 (Heap) 上,並且沒有實作 Copy Trait
。Copy Trait
的型別,賦值操作會導致所有權的「移動」(Move)。mocha.name
的所有權從 mocha 實例移動到了新的變數 new_coffee 身上。此時,new_coffee
成為了這個 String 的新擁有者。println!("{}", mocha.name);
— 為何會出錯?
因為 mocha.name
的所有權已經在上一行被移走了,所以 mocha 實例不再擁有 name 欄位的數據。
當你嘗試再次存取 mocha.name
時,Rust 的編譯器會阻止你,因為你正在嘗試使用一個已經被移動走的值。這就是 Rust 如何在編譯時期就保證記憶體安全的。編譯器會提示類似「borrow of moved value: mocha.name」的錯誤。
部分移動 (Partial Move)
值得注意的是,只有 name 欄位的所有權被移走了。mocha 的其他欄位,如 price
(型別 i32
) 和 is_hot
(型別 bool
),因為它們的型別都實作了 Copy Trait
,所以在存取時會進行「複製」(Copy) 而不是「移動」。因此,在 mocha.name
被移走後,你仍然可以存取 mocha.price
和 mocha.is_hot
。這種情況被稱為「部分移動」。
如果你只是想讀取 mocha.name 的值而不轉移其所有權,你可以「借用」它:
let new_coffee_ref = &mocha.name; // & 創建了一個引用 (借用),所有權沒有移動
println!("{}", new_coffee_ref);
println!("{}", mocha.name); // 這行現在可以正常運作了!
建立結構體實例後,有時你會需要修改其中的欄位值。這在 Rust 中是允許的,但有一個前提:你必須使用 mut
關鍵字將實例標記為可變。請看以下範例:
let mut beverage = Coffee {
price: 150,
name: String::from("Mocha"),
is_hot: true,
};
引用前面的例子,並將let mocha = Coffee{..}
的變數改掉,加入mut
,此時的 let mut beverage
表示 beverage
這個變數是可變的,我們才能修改它內部的欄位。接下來我們分別對三個欄位做了修改:
beverage.price = 170;
beverage.name = String::from("Caramel Macchiato");
beverage.is_hot = false;
每一行的語法都是:實例變數.欄位 = 新值;
。
price
的數值從 150 改為 170。name
被賦予一個新的 String
,這是因為 name
是堆積型別(heap type)。is_hot
被設為 false
,表示這杯飲料不再是熱的。最後我們使用 println!
印出新的欄位值,這也再次驗證我們已成功修改了原始資料。
概念 | 說明 |
---|---|
Struct 定義 | 使用 struct 名稱 { 欄位: 型別, ... } 建立自訂型別 |
實例創建 | 使用 {} 並指定所有欄位值 |
欄位存取 | 使用 實例.欄位 方式 |
擁有權移轉 | 若欄位型別為 String 等非 Copy 型別,讀取會移走所有權 |