前言
這篇文章主要簡單介紹在 rust 中我個人認為非常好用且常用的兩種型別 enum 以及 struct。
struct
基本使用
struct 我個人認為蠻像 js 中的 object ,就是可以用一些欄位來描述一個資料。 首先用 struct 來宣告,然後加上名稱以及每個欄位的名稱以及型別,乍看之下跟 ts 的 interface 一模一樣。
struct User {
active: bool,
userName: String,
email: String,
}
那我們宣告一個 struct 後,接著就是創造它的 instance :
let user1 = User {
active: true,
email: String::from("[email protected]"),
userName: String::from("foo"),
};
如果剛好欄位名稱跟值所使用的變數名稱一樣就可以直接縮寫
fn build_user(email: String, userName: String) -> User {
User {
userName,
email,
active: false,
}
}
我們在初始化 struct 時有一個重點是所有欄位都要被初始化,也就是不能這樣使用:
// ❌ 這段 code 有錯
let user2 = User {
email: String::from("[email protected]"),
userName: String::from("foo"),
};
struct update syntax
如果要復用一個舊的 struct 就只需要在新的 struct 的初始化時使用 .. (特別注意這裡是兩個.)這個被稱為 struct update syntax 的功能。即可。
let user1 = User {
active: true,
email: String::from("[email protected]"),
userName: String::from("foo"),
};
let user2 = User {
active: true,
..user1
};
需要注意的是 ..user1 一定要放在最後,且不需要在句尾加上,
struct update syntax 其實跟 variable assignment ( = ) 很像,我們一樣用user1 、user2當作例子
// ❌ 這段 code 有錯
//下面這一行是一個macro,目的是為實現`Debug` trait,但本篇文章並不會特別說明這個語法。
#[derive(Debug)]
struct User {
active: bool,
userName: String,
email: String,
}
fn main() {
let user1 = User {
active: true,
email: String::from("[email protected]"),
userName: String::from("foo"),
};
let user2 = User {
userName: String::from("foo2"),
..user1
};
println!("{:?}, {:?}",user1,user2);
}
這段程式之所以有錯誤的原因我們也能從 compiler 給的訊息得知
let user2 = User {
| ______________-
18 | | userName: String::from("foo2"),
19 | | ..user1
20 | | };
| |_____- value partially moved here
21 |
22 | println!("{:?}, {:?}",user1,user2);
| ^^^^^ value borrowed here after partial move
因為 user1 已經有 borrow 值出去,所以這邊沒辦法再把 ownership 給 println! ,主要原因是 User 中的 email 是 String type ,而在 rust 中 String 是沒有實作 Copy trait ,導致在做 variable assignment 時,會是 move 而不是 copy。
但如果我們要改用 &str 來取代 String 就會遇上 lifetime 這個比起 ownership 我覺得更勸退初學者的概念。(未來有機會再寫成文章)
mutable
rust 裡的 struct 並無法做到單一欄位的 mutable 所以我們只能在宣告 struct 時加上 mut
然後我們只要對我們想要更改的欄位直接覆寫就好了
struct User {
active: bool,
userName: String,
email: String,
}
fn main() {
let mut user1 = User {
active: true,
userName: String::from("Todd"),
email: String::from("[email protected]"),
};
user1.userName = String::from("Todd2");
println!("The user is {}", user1.userName,); // The user is Todd2
}struct method
method 就很像 function 一樣,只是他是針對一個 struct (精確來說不只 struct 可以使用 method )去說明只有它可以使用這些 function。
首先我們使用 impl 語法並在後面接我們被實作的 method 的 struct
接下來就是實作 function 本身,就跟我們平常使用 fn 一樣
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}從上面看到 我們 impl Rectangle 後我們就可以在 rect1 直接使用 area() ,這裡需要注意的是 $self 其實是 self:&Self (大小寫注意)的縮寫。
也因為這是 function 所以這裡也是有 ownership 概念的,我們先把 &self 改為 self:Self 故意將 self move 到 fn 裡。
// ❌ 這段 code 有錯
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(self: Self) -> u32 {
self.width * self.height
}
fn perimeter(self: Self) -> u32 {
2 * (self.width + self.height)
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.\n
The perimeter of the rectangle is {} square pixels.,",
rect1.area(),
// ------ `rect1` moved due to this method call
rect1.perimeter()
// ^^^^^ value used here after move
);
}從註解中的錯誤訊息就可以很清楚的知道,因為我們已經將 rect1 的 ownership move 到 area() 裡,所以下一行的 perimeter() 就無法使用 rect1 了。
enum
基本使用
enum 在字面上的意思就是枚舉的意思,我的個人理解是它在描述**「某個值只能取特定值之一的情況」**,像是撲克牌只有四種花色、硬幣只有正反面。
我認為 rust enum 比起 ts 好用許多,當然一部份是因為 rust 本身提供其他語法讓 enum 具有那麼強的能力,像是 pattern matching ,但光是 rust 本身給 enum 就有許多好用的特性了
首先我們定義一個 enum 只需要 使用 enum 去宣告
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;使用起來只需要使用 IpAddrKind::{variants}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;現在回頭想想上面的範例,如果我們要用 IpAddrKind 去描述一個 ip 位址顯然是不夠的,所以我們可能直接是想到用 enum 加上 struct 來輔助我們
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};我們宣告了 IpAddr 這個 struct 來幫助我們描述這兩種 ip 位址的形式。但這顯然不夠簡潔因為我們早就可以確定 IpAddrKind::V4 一定是四個數字,但如果我直接把 address 改為 (u8,u8,u8.u8) 來描述時我在 kind 是 IpAddrKind::V6 又會有問題 ,總不能直接拆成兩個 struct 吧?那有沒什麼辦法更加的優雅描述這種情況呢?
這時 rust 的 enum 就提供一個很好的特性:enum 的 variants 是允許擁有數值的我覺得可能比較相近的比喻是**「盒子裡面裝著一個數值」**。這可以讓我們更加簡單的描述這種類別的數值。
所以可以將 IpAddr 改為:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let localhost_v4 = IpAddr::V4(127, 0, 0, 1);
let localhost_v6 = IpAddr::V6(String::from("::1"));但其實在 std::net::IpAddr 這個標準函式庫裡就有幫我們示範了這個例子大神們會怎麼實現
首先他把 IpAddr 的 V4 及 V6 分別改接 struct : Ipv4Addr 、Ipv6Addr
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
let localhost_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let localhost_v6 = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));那在 std::net::Ipv4Addr 及 std::net::Ipv6Addr 有 impl new 這個 method 我們就可以直接建立好這個 struct 並放入 enum 裡。
簡單的 pattern matching
其實 pattern matching 並不是只有用在 enum 但大部分時候我們使用 enum 時會一起使用 pattern matching ,畢竟 enum 就是代表 **「某個值只能取特定值之一的情況」**所以我們常常需要判斷這値處於哪一種特定值。
use std::net::{Ipv4Addr, Ipv6Addr};
// std::net::IpAddr 本身其實就有 impl Debug trait
// 但為了下面的範例這邊就自己自己宣告 IpAddr
#[derive(Debug)]
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
fn main() {
let localhost_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let localhost_v6 = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
println!("The ip is {:?}", localhost_v4); // V4(127,0,0,1)
if let IpAddr::V4(i) = localhost_v4 {
println!("The ip is {:?}", i); // 127,0,0,1
}
}我們可以看到我們可以利用 if let (一種 pattern matching 語法)將 enum 這個**「盒子」V4** 給打開並取出裡面的值 127.0.0.1 。
當然 pattern matching 在 rust 遠遠不止如此,未來有機會再詳細說明 xD 。
參考資料
Using Structs to Structure Related Data - The Rust Programming Language (rust-lang.org)
Enums and Pattern Matching - The Rust Programming Language (rust-lang.org)
