結構體(struct)是 Rust 中的一種自定型別,用來封裝多個相關數值成一個組合的型別,每個數值可以是不同的型別,到這邊和元組(Tuple)是一樣的,接下來不同的是,結構體必須要為每個數值對應欄位(fields)命名來表達數值的意義,同時這些欄位也可以用來指定或存取實例(instance)中的值,不像元組必須要要依賴資料的順序。
換句話說,類似 JavaScript 的 Class 的概念。
要定義一個結構體就用struct
關鍵字,後面接大括號,裡面是用key:value
的方式定義每個欄位的名稱以及對應的型別,以下我們定義一個Cat
的型別。
struct Cat {
name: String,
age: u8,
weight: f64
}
我們定義的結構體就像是一個餅乾模具,用這個模具成形的餅乾就稱為實例。
要用建出這個結構體實例的方式如下。
fn main() {
let my_cat = Cat {
name: String::from("Milo"),
age: 2,
weight: 4.6,
};
println!("My cat's name: {}", my_cat.name);
}
實例後面用 .
就可以用欄位名稱連結到我們的目標數值。
如果我們要更改實例上某個欄位的數值的話,和一般變數一樣一開始就要給 mut
關鍵字。
另外整個實例上所有的欄位上能不能改的設定都要是一樣的,也就是說不允許有的欄位可變有的不可變,要嘛全部可以要嘛全部不行。
fn main() {
let mut my_cat = Cat {
name: String::from("Milo"),
age: 2,
weight: 4.6,
};
println!("My cat's original name: {}", my_cat.name);
my_cat.name = String::from("Luna");
println!("My cat's current name: {}", my_cat.name);
}
Rust 也借鑒了近代程式的一些方便的語法,從 JavaScript 轉過來覺得真熟悉。
簡單說就是如果欄位名稱和變數或參數名相同不用再寫一次。
我們先寫一個完整的寫法:
// 完整寫法
fn adopt_cat(name: String, age: u8) -> Cat {
Cat { name: name, age: age, weight: 5.5 }
}
結構體產生的實例也可以當成函數的返回值,產生實例的寫法本身就是表達式。
因為我們傳的參數名稱age
和name
和 Cat
上面對應的欄位名稱相同,就可以簡寫成以下:
// **欄位初始化簡寫, struct 內的 name 和 age 寫法可以簡化**
fn adopt_cat(name: String, age: u8) -> Cat {
Cat { name, age, weight: 5.5 }
}
這個語法可以用既有實例上的資訊來產生新的實例,然後可以針對我們想要特定欄位數值修改。
語法就是在建立實例的大括號內用..
後面接既有的實例。
修改的欄位可以不用照順序,但..
一定要放在最後一個,因為它代表的意義是剩下沒指定的欄位都會用和提供的實例相同的值,也就是提供實例上的數值都會當成預設值的意思。
fn main() {
let my_cat1 = Cat {
name: String::from("Milo"),
age: 5,
weight: 4.6
};
let my_cat2 = Cat {
name: String::from("Luna"),
..my_cat1
};
println!("My 1st cat's name: {}", my_cat1.name);
println!("My 2nd cat's name: {}", my_cat2.name);
}
這樣我們就產生兩個Cat
的實例,除了name
,兩者其他所有欄位數值是相同的。
接著複習一下所有權還有生命週期,沒錯,函數有的它也跑不掉。
我們先把Cat
再多加欄位owner
。
struct Cat {
name: String,
owner: String,
age: u8,
weight: f64
}
fn main() {
let my_cat1 = Cat {
name: String::from("Milo"),
owner: String::from("Blue"),
age: 5,
weight: 4.6
};
let my_cat2 = Cat {
name: String::from("Luna"),
..my_cat1
};
println!("My 1st cat's owner: {}", my_cat1.owner);
println!("My 2nd cat's owner: {}", my_cat2.owner);
}
上面這段程式碼看起來合理,owner
是同一個所以沿用原有的實例的,但…
編譯錯誤了😂:
$ cargo run
error[E0382]: borrow of moved value: `my_cat1.owner`
--> src/main.rs:30:39
|
25 | let my_cat2 = Cat {
| ___________________-
26 | | name: String::from("Luna"),
27 | | ..my_cat1
28 | | };
| |_____- value moved here
29 |
30 | println!("My 1st cat's name: {}", my_cat1.owner);
| ^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `my_cat1.owner` has type `String`, which does not implement the `Copy` trait
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
這是因為結構體更新語法和賦值意義上是相同的,所以 my_cat1.owner 的所有權被給出去了,其他欄位還是可以正常取得,name
的話是因為我們給它另外一個name
資料,age
和weight
是因為它們都是有實作Copy
特徵的型別,實際上會複製了一份資料所以不影響到原有實例上數值的所有權。
解決方法有兩個:
Clone
或另外新增一個owner
資料給my_cat2
,這類方式都會多佔另外一個記憶空間。owner
的型別改成參考,其實更合理,我們不需要每次有新的cat
就要跟著多一個owner
出來。我們試著改成第二種做法之後,有點熟悉的生命週期錯誤又出現啦XD
struct Cat {
name: String,
owner: &str,
age: u8,
weight: f64
}
fn main() {
let owner= String::from("Blue");
let my_cat1 = Cat {
name: String::from("Milo"),
owner: &owner,
age: 5,
weight: 4.6
};
let my_cat2 = Cat {
name: String::from("Luna"),
..my_cat1
};
println!("My 1st cat's owner: {}", my_cat1.owner);
println!("My 2nd cat's owner: {}", my_cat2.owner);
}
$ cargo run
error[E0106]: missing lifetime specifier
--> src/main.rs:11:12
|
11 | owner: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
9 ~ struct Cat<'a> {
10 | name: String,
11 ~ owner: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
但沒事,我們現在已經理解這個錯誤的意思了。
結構體有用到的參考都需要我們顯式標註生命週期,所以和函數一樣,我們在結構體名稱後面加上<>
來傳入泛型生命週期參數,這樣在定義內部結構我們就可以用這個泛型生命週期參數,修改一下就可以正常編譯囉。
struct Cat<'a> {
name: String,
owner: &'a str,
age: u8,
weight: f64
}
和函數有一個比較大的差別是,結構體不適用於生命週期省略,所以即使只有一個欄位用到參考,這個參考也要加入生命週期詮釋,或更簡單的說:結構體用到參考的欄位型別都要加上生命週期詮釋。
結構體不適用生命週期省略的原因大致上有:
總結來說,結構體生命週期省略的限制是 Rust 設計者們為了保證程式碼的安全性和可靠性而做出的一種謹慎選擇。
接著再介紹兩種特殊的結構體。
這種結構體他沒有欄位名稱,只會定義欄位型別,特性和元組幾乎相同,最大的差別是他可以自訂本身的型別及名稱,用來和一般的tuple
或其他的tuple structs
做區別。舉例來說,我們用來表示顏色可以用 3個數字表達 RGB 的組合,表示座標可以用 3 個數字表達 XYZ 的組合,但是我們在用的時候不希望混淆這兩種類型。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn describe_color(color: Color) {
println!("Color - Red: {}, Green: {}, Blue: {}", color.0, color.1, color.2);
}
fn describe_point(point: Point) {
println!("Point - X: {}, Y: {}, Z: {}", point.0, point.1, point.2);
}
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
describe_color(black);
describe_point(origin);
}
tuple structs 的定義方式是關鍵字struct
然後定義名稱和要定義的元組,所以用小括號。
建出來的實例跟元組一樣可以用 .
來加上索引取得各數值。上面的例子可以看出來即使元組的各個元素定義的型別是完全一樣的,我們依然可以區分兩者,Color
和Point
是不一樣的型別,如果函數的參數用錯型別的話編譯器會直接報錯的。
總結會用到tuple structs
的情境就是:
既然有類似元組的結構體,當然也會有類似單元型別 ()
的結構體,這是一種沒有任何欄位的結構體。用來表示「沒有具體數值」的情況。
主要的用途:
寫法也很簡單,關鍵字struct
後面定義名稱直接分號結束,不用任何括號,因為沒有要儲存任何數據,之後就和一般結構體一樣的方式建立它的實例。
struct AdminAccess;
struct GuestAccess;
fn main() {
let admin = AdminAccess;
let guest = GuestAccess;
perform_admin_action(admin);
}
fn perform_admin_action(_admin: AdminAccess) {
println!("Admin action performed successfully!");
}
舉例來說,上面的程式碼就讓函式可以根據不同的型別做檢查,不符合的型別會報錯。我們可以把這種權限標記在不同的使用者上,讓我們的程式碼更具可讀性和可維護性,
再之後用於標識特徵的實作也會用到unit-like structs
,之後介紹到的時候會更瞭解它的重要性,目前的部分就先介紹到這樣。
今天介紹了從基本的結構體到unit-like structs
的應用以及結構體處理所有權和生命週期的情況,下一篇我們會繼續延伸結構體來介紹方法(method),概念上就是 JavaScript 裡 class 有的那個 method 沒錯。