定義並介紹結構體
結構體和我們前面學習的元組類似,結構體中的每一項都可以是不同的數據類型。和元組不同的地方在於,我們需要給結構體的每一項命名。結構體較元組的優勢是:我們聲明和訪問數據項的時候不必使用索引,可以直接使用名字。
聲明結構體
我們直接看一個結構體的例子:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
結構體使用關鍵字 struct 開頭,緊跟結構體的名字,之后就是大括號包裹的多條結構體數據項,每個數據項由名字和類型組成,我們把每個數據項稱為字段。
結構體實例化
我們聲明了一個結構體后,如何使用它呢?接下來創建一個結構體的實例:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
可以看到,創建結構體實例 (結構體實例化) 是直接以結構體名字開頭,之后就是大括號包裹的鍵值對。這些鍵值對順序和聲明結構體的順序無關,換句話說,聲明結構體就是定義一個通用的模版,結構體實例化就是給模版填充值。
結構體數據的存取
創建了結構體實例,那我們應該如何存取實例中的數據呢?比如我們要獲取郵箱信息,可以 user1.email 獲取郵箱內容,如果實例是可變的,我們可以直接給它賦值。直接看個賦值的例子吧:
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
這個實例整個都是可變的,如果我只想修改 email 和 username 兩個字段,而不想修改其它的字體,應該怎么辦呢?
修改部分字段
要知道,rust 不允許我們只把部分字段標記為可變。那我們可不可以把這個結構體放在函數中,讓函數返回一個新的實例呢?看例子:
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
在這個例子中,函數參數名字和結構體字段名字是相 的,如果有很多字段,一個一個地寫名字和參數是很無聊的,不過,rust 為我們提供了簡寫的方式
結構體字段初始化簡寫
我們直接看例子吧:
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
email 和 username 的結構體字段名字和函數傳入的參數變量的名字是相同的,我們可以只寫一遍。
結構體更新
如果舊的結構體實例中的一部分值修改,使之變成一個新的實例,使用結構體更新語法會更加方便。如果在 user1 的基礎上修改 email 和 username 而不改變其他的值,我們通常會這樣寫:
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
如果我們使用了結構體更新語法,創建新結構體就變成了這樣:
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
利用 ..
語法達到了和前面案例相同的結果。
元組結構體
我們也可以定義看起來像元組的結構體,我們稱它為元組結構體。元組結構體只定義字段的類型而不定義字段的名字,它主要用於給整個元組一個名字來區分不同的元組。直接看例子:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
定義了黑色和原點,顯然二者的類型不相同,雖然聲明的字段類型相同,但是二者是使用不同的元組結構體實例化的。
空結構體
我們也可以定義空結構體,它不包含任何字段。詳細內容后文再聊。
寫個關於結構體的例子
為了學習什么時候用到結構體,我們來寫一個計算長方形面積的程序。我們從使用簡單變量寫起,一直寫到使用結構體為止。
編寫項目
我們先來創建一個名叫 rectangles 的項目,然后寫一個通過寬高計算面積的函數。
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"長方形的面積是 {}。",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
我們運行的結果是:
cargo run
Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.51s
Running `target/debug/rectangles`
長方形的面積是 1500。
我們調用 area 函數完成了對長方形面積的計算,但是描述長方形的寬高是分開的,我們能不能想個辦法,把兩個值變成一個值?很容易想到的辦法就是元組,對,沒錯,是元組。
利用元組重構項目
我們直接看重構完成后的代碼:
fn main() {
let rect1 = (30, 50); // 定義元組
println!(
"長方形的面積是 {}。",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
現在只有一個值了,但是又有了一個新的問題:元組沒有名字,計算面積還好,元組中的兩個值混了也沒事,如果是把這個長方形畫出來,那就得記着 0 位置是寬,1 位置是高,別人調用我們的代碼時,別人也得記着這個順序。這是很不友好的,那應該怎么解決呢?
利用結構體重構項目
我看來看重構后的代碼:
struct Rectangle { // 定義結構體
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 }; // 結構體實例化
println!(
"長方形的面積是 {}。",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
現在只有一個參數了,而且參數也有了實際的含義了,似乎完成了我們的目標。但是 area 函數只能計算長方形的面積,我們希望這個函數盡可能地在 Rectangle 結構體內部,因為它不能處理其它的結構體。那我們應該如果做呢?我們可以把該函數轉變成 Rectangle 結構體的方法。在此之前,我們先看一個調試程序的小技巧。
打印結構體
在我們調試程序的時候,經常想看一下結構體每個字段的值是什么,如果直接打印結構體會報錯,比如:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 是: {}", rect1); // 報錯: `Rectangle` cannot be formatted with the default formatter
}
rust 為我們提供了打印的方法,在結構體定義的上方加入 #[derive(Debug)]
聲明,在打印的大括號中加入 :?
就可以了,看例子:
#[derive(Debug)] // 這里加入聲明
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 是: {:?}", rect1); // 這里加入打印的格式
}
運行的結果是:
cargo run
Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/rectangles`
rect1 是: Rectangle { width: 30, height: 50 }
這個結構體的輸出很不美觀,我們調整一下,讓結構體可以結構化輸出,只需要把 {:?}
改成 {:#?}
即可,然后輸出就變成了:
cargo run
Compiling rectangles v0.1.0 (/Users/shanpengfei/work/rust-work-space/study/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/rectangles`
rect1 是: Rectangle {
width: 30,
height: 50,
}
后文會詳細介紹 derive 聲明。
結構體方法
方法和函數類似,都是以關鍵字 fn 打頭,后接方法名、參數和返回值,最后是方法體。方法和函數不同之處在於:方法定義在結構體的上下文中,方法的第一個參數是 self (self 代表結構體方法調用者的實例)。
定義方法
我們來修改 area 方法,把它變成 Rectangle 結構體的方法,如下:
#[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!(
"長方形的面積是 {}。",
&rect1.area()
);
}
使用 impl 關鍵字和結構體名字 Rectangle 來定義 Rectangle 的上下文,然后把 area 函數放進去,就把函數的第一個參數修改成 self,在主函數中,可以讓 rect1 實例直接調用 area 方法。
對於方法和第一個參數,我們使用 &self 來代替 rectangle: &Rectangle,因為在 impl Rectangle 聲明的上下文中,rust 知道 self 代表的是 Rectangle,但是還需要在 self 前面加上 & 符號,意思是借用 Rectangle 的不可變實例。如果要借用 Rectangle 的可變實例,參數需要寫成 &mut self。
使用方法較函數的優勢在於:添加方法的時候可以直接使用結構體方法語法,而不必要在每個函數中重復寫實例的類型。我們可以把結構體對應的所有方法都寫在結構體的上下文中,當我們為別人提供庫函數的時候不必要在很多地方尋找需要的函數。
多參方法
我們再寫一個例子,第一個長方形能不能裝下第二個長方形,如果能裝下,就返回 true,否則返回 false,實例代碼如下:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
println!("rect1 裝下 rect2? {}", rect1.can_hold(&rect2));
println!("rect1 裝下 rect3? {}", rect1.can_hold(&rect3));
}
我們先來看運行結果:
rect1 裝下 rect2? true
rect1 裝下 rect3? false
我們在 Rectangle 上下文中定義第二個方法 can_hold,該方法借用另一個 Rectangle 實例作參數,我們需要告訴調用者參數的類型。
關聯函數
在結構體上下文中也可以定義不含有 self 參數的函數,這種函數被稱為關聯函數。這里叫函數,不叫方法,因為這些函數不是使用結構體實例調用的,而是使用雙冒號調用,比如之前使用的 String::from 就是一個關聯函數。
關聯函數常常被用於返回結構體實例的構造函數。例如,我們可以提供一個關聯函數來生產 Rectangle 結構體的實例。在這里,我們假設寬度是相等的,我們就可以使用一個參數來代替兩個參數了,如下:
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
使用雙冒號 ::
語法來調用關聯函數,舉個簡單的例子 let sq = Rectangle::square(3);
,即 結構體::關聯函數
。
多個 impl 模塊
每個結構體都允許使用多個 impl 聲明的上下文,例如:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
這種語法是有效的,后文學習中可能會使用到,知道有這種語法就好了。
歡迎閱讀單鵬飛的學習筆記