Rust 學習筆記:結構體(struct)
- Rust 學習筆記:結構體(struct)
- 結構體的定義和實例化
- 使用字段初始化簡寫
- 用 Struct Update 語法從其他實例創建實例
- 使用沒有命名字段的元組結構來創建不同的類型
- 沒有任何字段的類單元結構
- 結構體的所有權
- 示例程序:Rectangle
- 用派生特性添加有用的功能
- Debug
- Copy, Clone
- 方法的語法
- 有更多參數的方法
- 關聯函數
- 多個 impl 塊
Rust 學習筆記:結構體(struct)
struct 是一種自定義數據類型,它允許將多個相關的值打包在一起并命名,從而組成一個有意義的組。
結構體的定義和實例化
要定義結構,輸入關鍵字 struct 并為整個結構命名。
然后,在花括號內定義數據塊的名稱和類型,我們稱之為字段。
struct User {active: bool,username: String,email: String,sign_in_count: u64,
}
要在定義結構后使用它,需要為每個字段指定具體的值,從而創建該結構的實例。
我們通過聲明結構體的名稱來創建實例,然后添加包含鍵:值對的花括號,其中鍵是字段的名稱,值是我們希望存儲在這些字段中的數據。我們不必按照在結構體中聲明字段的順序指定字段。換句話說,結構定義就像是該類型的通用模板,實例用特定的數據填充該模板,以創建該類型的值。
fn main() {let user1 = User {active: true,username: String::from("someusername123"),email: String::from("someone@example.com"),sign_in_count: 1,};
}
為了從結構體中獲得特定的值,我們使用點表示法。例如,要訪問這個用戶的電子郵件地址,我們使用 user1.email。如果實例是可變的,我們可以通過使用點表示法和賦值到一個特定的字段來改變值。
fn main() {let mut user1 = User {active: true,username: String::from("someusername123"),email: String::from("someone@example.com"),sign_in_count: 1,};user1.email = String::from("anotheremail@example.com");
}
注意,整個實例必須是可變的;Rust 不允許我們只將某些字段標記為可變的。
結構體的新實例可以作為函數體中的最后一個表達式,以隱式返回該新實例。
fn build_user(email: String, username: String) -> User {User {active: true,username: username,email: email,sign_in_count: 1,}
}
username: username 這種寫法有點乏味。如果結構體有更多字段,重復每個名稱會變得更加煩人。幸運的是,有一種方便的速記方法!
使用字段初始化簡寫
字段 init 速記語法允許將同名的字段和參數進行簡化。
例如,因為 email 字段和 email 參數有相同的名字,所以我們只需要寫 email 而不是 email: email。
fn build_user(email: String, username: String) -> User {User {active: true,username,email,sign_in_count: 1,}
}
用 Struct Update 語法從其他實例創建實例
創建一個結構體的新實例,該實例包含另一個實例的大部分值,但更改了一些值,這通常很有用。
不使用 Struct Update 語法,每個字段都需要設置。
fn main() {// --snip--let user2 = User {active: user1.active,username: user1.username,email: String::from("another@example.com"),sign_in_count: user1.sign_in_count,};
}
使用 Struct Update 語法,指定未顯式設置的其余字段應具有與給定實例中的字段相同的值。
fn main() {// --snip--let user2 = User {email: String::from("another@example.com"),..user1};
}
..user1
必須在最后指定任何剩余的字段應該從 user1 中的相應字段獲取它們的值。
請注意,struct update 語法就像使用 = 賦值一樣,這是因為它移動數據,會發生所有權的轉移。
在本例中,在創建 user2 之后,我們不能再將 user1 作為一個整體使用,因為 user1 的 username 字段中的 String 被移到了 user2 中。
如果我們只使用 user1 的 active 和 sign_in_count 值,那么在創建 user2 之后,user1 仍然有效。這是因為 active 和 sign_in_count 都是實現Copy Trait 的類型,這些變量會復制,并不移動。
使用沒有命名字段的元組結構來創建不同的類型
Rust 還支持類似于元組的結構,稱為元組結構。沒有字段的名稱,只有字段的類型。
要定義元組結構,首先使用struct關鍵字和結構名,然后是元組中的類型。例如,這里我們定義并使用了兩個名為 Color 和 Point 的元組結構體:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);fn main() {let black = Color(0, 0, 0);let origin = Point(0, 0, 0);
}
注意,即使 Color 和 Point 這兩種類型都由三個 i32 值組成,但它們的實例不能互相賦值。
沒有任何字段的類單元結構
沒有任何字段的結構體被稱為類單元結構。
當需要在某些類型上實現 trait,但沒有任何想要存儲在類型本身中的數據時,類單元結構可能很有用。
下面是一個聲明和實例化一個名為 AlwaysEqual 的類單元結構的例子:
struct AlwaysEqual;fn main() {let subject = AlwaysEqual;
}
結構體的所有權
在之前的 User 結構體定義中,我們使用了 String 類型而不是 &str 字符串切片類型。這是一個經過深思熟慮的選擇,因為我們希望這個結構體的每個實例都擁有它的所有數據,并且只要整個結構體有效,這些數據就有效。
結構體也可以存儲對其他對象擁有的數據的引用,但這樣做需要使用生命周期,以確保結構體引用的數據在該結構體存在的時間內有效。假設在一個結構體中存儲一個引用而不指定生命周期,如下所示。
struct User {active: bool,username: &str,email: &str,sign_in_count: u64,
}fn main() {let user1 = User {active: true,username: "someusername123",email: "someone@example.com",sign_in_count: 1,};
}
報錯:缺少生命周期標識符
示例程序:Rectangle
創建一個名為 Rectangle 的結構體,存儲矩形的寬度和高度。
再創建一個函數,入參為結構體對象,計算矩形的面積。
struct Rectangle {width: u32,height: u32,
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("The area of the rectangle is {} square pixels.",area(&rect1));
}fn area(rectangle: &Rectangle) -> u32 {rectangle.width * rectangle.height
}
注意,訪問借用的結構體實例的字段并不會移動字段值,這就是為什么經常看到借用結構體的原因。
用派生特性添加有用的功能
Debug
在調試程序時打印一個 Rectangle 實例并查看其所有字段的值是很有用的。
struct Rectangle {width: u32,height: u32,
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("rect1 is {}", rect1);
}
println! 宏可以進行多種格式化,默認情況下,大括號告訴 println! 使用稱為顯示的格式:用于直接最終用戶使用的輸出。到目前為止,我們看到的基本類型都默認實現了 Display,但是結構體沒有提供 Display 的實現(與 println! 以及 {} 占位符)。
從報錯信息中我們能得到提示:
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
輸入說明符 :? 在花括號內告訴 println! 我們希望使用一種名為 Debug 的輸出格式。Debug 特性使我們能夠以一種對開發人員有用的方式打印結構體,這樣我們在調試代碼時就可以看到它的值。
只是這樣還不夠,還是報錯:
error[E0277]: `Rectangle` doesn't implement `Debug`
...
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Rust 確實包含打印調試信息的功能,但我們必須顯式地選擇使該功能對結構體可用。為此,我們在結構定義之前添加 #[derive(Debug)]
。
#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}fn area(rectangle: &Rectangle) -> u32 {rectangle.width * rectangle.height
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};println!("rect1 is {rect1:?}");println!("The area of the rectangle is {} square pixels.",area(&rect1));
}
現在,當我們運行程序時,我們不會得到任何錯誤,并且我們將看到以下輸出:
冒號和問號之間再加一個 #,即 :#?,可以使得打印的值更有格式,這在結構體中字段多時比較有用。
使用 Debug 格式打印值的另一種方法是使用 dbg! 宏,它接受一個表達式的所有權(與 println! 相反,它接受一個引用)。
注意,調用 dbg! 宏打印到標準錯誤控制臺流(stderr),println! 則打印到標準輸出控制臺流(stdout)。
Copy, Clone
復制和克隆屬性,可以讓結構體的方法不再奪取結構體實例的所有權。
方法的語法
方法類似于函數:我們用 fn 關鍵字和一個名稱來聲明它們,它們可以有參數和返回值。
與函數不同,方法是在結構體(或 enum 或 trait 對象)的上下文中定義的,它們的第一個參數總是 self,它表示調用方法的結構體的實例。
#[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());
}
為了在 Rectangle 的上下文中定義函數,我們為 Rectangle 啟動了一個 impl(實現)塊。這個 impl 塊中的所有內容都將與 Rectangle 類型相關聯。然后將 area 函數移動到 impl 花括號內,并將簽名中的第一個(在本例中是唯一一個)參數更改為 self,并將函數體中的所有參數更改為 self。
在 area 的簽名中,我們使用 &self 代替 rectangle: &Rectangle。&self 實際上是 self: &Self 的縮寫。在一個 impl 塊中,Self 類型是該 impl 塊所針對的類型的別名。方法的第一個參數必須有一個名為 self 的 Self 類型的參數,因此 Rust 允許在第一個參數點只使用名稱 self 來縮寫它。
注意,我們仍然需要在 self 前面使用 & 來表示這個方法借用了 self 實例,就像我們在 rectangle: & rectangle 中所做的那樣,因為這里我們不想占有所有權。
除了提供方法語法和不必在每個方法的簽名中重復 self 的類型之外,使用方法而不是函數的主要原因是為了組織。我們把一個類型的實例所能做的所有事情都放在了一個 impl 塊中,而不是讓未來的代碼用戶在我們提供的庫的各個地方搜索 Rectangle 的功能。
注意,我們可以選擇給一個方法賦予與結構體的一個字段相同的名稱。例如,我們可以在 Rectangle 上定義一個同樣命名為 width 的方法:
impl Rectangle {fn width(&self) -> bool {self.width > 0}
}fn main() {let rect1 = Rectangle {width: 30,height: 50,};if rect1.width() {println!("The rectangle has a nonzero width; it is {}", rect1.width);}
}
這里,我們選擇讓 width 方法在實例的 width 字段值大于 0 時返回 true,在值小于等于 0 時返回 false。當我們使用帶括號的 width, Rust 知道我們指的是方法寬度;當我們不使用括號時,Rust 知道我們指的是字段寬度。
通常(但并非總是),當我們給一個方法賦予與字段相同的名稱時,我們希望它只返回字段中的值,而不做任何其他事情。像這樣的方法被稱為 getter, Rust 不像其他語言那樣為 struct 字段自動實現它們。getter 很有用,因為您可以將字段設置為私有,而將方法設置為公共,從而使對該字段的只讀訪問成為類型的公共API的一部分。
在 C/C++ 中,調用方法使用兩種不同的操作符:object->something() 或 (*object).something()。
Rust 沒有與 -> 操作符等價的操作符;相反,Rust 有一個稱為自動引用和解引用的特性。它是這樣工作的:當你用 object.something() 調用一個方法時,Rust 會自動添加 &,&mut 匹配方法的簽名。換句話說,以下內容是相同的:
p1.distance(&p2);
(&p1).distance(&p2);
第一個看起來干凈多了。這種自動引用行為之所以有效,是因為方法有一個明確的接收者——self 類型。事實上,Rust 為方法接收者提供了隱含的借用。
有更多參數的方法
讓我們通過在 Rectangle 結構體上實現第二個方法:can_hold。
這一次,我們想要一個矩形的實例接受另一個矩形的實例,如果第二個矩形可以完全適合 self(第一個矩形),返回 true;否則,它應該返回 false。
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}
}
can_hold 函數將以一個不可變的借用另一個矩形作為第二個參數,比較后返回一個布爾值。
關聯函數
在 impl 塊中定義的所有函數都稱為關聯函數,因為它們與以 impl 命名的類型相關聯。我們可以定義不以 self 作為第一個參數的關聯函數(因此不是方法),因為它們不需要使用該類型的實例。
我們已經使用過一個這樣的函數:String::from 函數,它定義在 String 類型上。
非方法的關聯函數通常用于將返回該結構的新實例的構造函數。這些通常被稱為 new,但 new 并不是一個特殊的名稱,也沒有內置到語言中。例如,我們可以選擇提供一個名為 square 的關聯函數,它將有一個維度參數,并使用它作為寬度和高度,從而使創建一個正方形矩形更容易,而不必兩次指定相同的值:
impl Rectangle {fn square(size: u32) -> Self {Self {width: size,height: size,}}
}
返回類型和函數體中的 Self 關鍵字是出現在 impl 關鍵字之后的類型的別名,在本例中是 Rectangle。
要調用這個關聯函數,可以使用 結構體名+ :: + 方法名,比如:
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}
}
這里沒有理由將這些方法分離到多個 impl 塊中,但這是有效的語法。
但在討論泛型類型和特征時,多個 impl 塊是有用的。