
前端小魔女
2023/02/08阅读:17主题:蔷薇紫
Rust学习笔记之结构体
❝我并不算金子,我是光本身
❞
大家好,我是「柒八九」。
今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「结构体」的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
你能所学到的知识点
❝❞
认识 Rust
结构体 「推荐阅读指数」 ⭐️⭐️⭐️⭐️如何使用结构体 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️ Rust
中使用方法 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
struct
,或者 structure
,是一个「自定义数据类型」,允许你命名和包装多个相关的值,从而形成一个有意义的组合
。
定义并实例化结构体
❝「结构体」和元组类似。和元组一样,「结构体的每一部分可以是不同类型」。但不同于元组,结构体「需要命名各部分数据以便能清楚的表明其值的意义」。由于有了这些名字,结构体比元组更灵活:「不需要依赖顺序来指定或访问实例中的值」。
❞
定义结构体,需要使用 struct
关键字并为整个结构体提供一个名字。「结构体的名字」需要描述它所组合的数据的意义。接着,在大括号
中,「定义每一部分数据的名字和类型」,我们称为 字段。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
一旦「定义了结构体后」,为了使用它,通过「为每个字段指定具体值来创建这个结构体的实例」。创建一个实例需要以结构体的名字开头,接着在大括号
中使用 key: value
「键-值对的形式」提供字段
-
key
是字段的名字
-
value
是需要存储在字段中的数据值
「实例中字段的顺序不需要和它们在结构体中声明的顺序一致」。换句话说,结构体的定义就像一个类型的通用模板
,而实例则会在这个模板中放入特定数据来创建这个类型的值。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("acb@example.com"),
username: String::from("前端柒八九"),
active: true,
sign_in_count: 1,
};
}
为了从结构体中获取某个特定的值,可以「使用点号」。如果我们只想要用户的邮箱地址,可以用 user1.email
。
要更改结构体中的值,如果结构体的实例是可变的
,我们可以使用点号并为对应的字段赋值。
fn main() {
let mut user1 = User {
email: String::from("abc@example.com"),
username: String::from("前端柒八九"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("789@example.com");
}
❝整个实例必须是可变的;
❞Rust
并「不允许只将某个字段标记为可变」。
可以在函数体的「最后一个表达式」中构造一个结构体的新实例,来隐式地返回这个实例。
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
变量与字段同名时的字段初始化简写语法
参数名与字段名都完全相同,我们可以使用字段初始化简写语法来重写 build_user
,这样其行为与之前完全相同,不过无需重复 email
和 username
了
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
使用结构体更新语法从其他实例创建实例
使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常很有用。这可以通过结构体更新语法实现。
fn main() {
// --snip--
let user1 = User {
email: String::from("abc@example.com"),
username: String::from("前端柒八九"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("789@example.com"),
sign_in_count: user1.sign_in_count,
};
}
我们为 email
设置了新的值,其他值则使用了实例中创建的 user1
中的同名值。
.. 语法
指定了「剩余未显式设置值」的字段应有与给定实例对应字段相同的值。
fn main() {
let user1 = User {
email: String::from("abc@example.com"),
username: String::from("前端柒八九"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("789@example.com"),
..user1
};
}
在 user2
中创建了一个新实例,其有不同的 email
值,不过 username
、 active
和 sign_in_count
字段的值与 user1
相同。「把..user1
必须放在最后」,以「指定其余的字段应从 user1
的相应字段中获取其值」。
使用没有命名字段的元组结构体来创建不同的类型
也可以定义与「元组」类似的结构体,称为元组结构体。「元组结构体」有着结构体名称提供的含义,但「没有具体的字段名,只有字段的类型」。
当你想给「整个元组取一个名字」,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
要定义元组结构体,「以 struct
关键字和结构体名开头并后跟元组中的类型」。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
black
和 origin
值的类型不同,因为它们是「不同的元组结构体的实例」。
❝定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型
❞
没有任何字段的类单元结构体
也可以定义一个没有任何字段的结构体!它们被称为类单元结构体,因为它们类似于 ()
。
类单元结构体
常常在你想要在某个类型上实现 trait
但「不需要在类型中存储数据的时候发挥作用」。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
如何使用结构体
我们编写一个「计算长方形面积的程序」。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。
使用单独变量
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"长方形面积为{}",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
使用元组重构
fn main() {
let rect1 = (30, 50);
println!(
"长方形面积为{}",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.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
}
定义了一个「结构体」并称其为 Rectangle
。在大括号中定义了字段 width
和 height
,类型都是 u32
。接着在 main
中,我们创建了一个具体的 「Rectangle 实例」,它的宽是 30
,高是 50
。
函数 area
现在被定义为接收一个名叫 rectangle
的参数,其类型是一个结构体 Rectangle
实例的「不可变借用」。希望借用结构体而不是获取它的所有权
,这样 main
函数就可以保持 rect1
的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &
。
通过派生 trait 增加实用功能
如果能够在调试程序时打印出 Rectangle
实例来查看其所有字段的值就更好了。尝试使用 println! 宏
。但这并不行。
println!
宏能处理很多类型的格式,不过,{}
默认告诉 println!
使用被称为 Display
的格式:意在提供给直接终端用户查看的输出。目前为止见过的「基本类型都默认实现了 Display」,因为它就是向用户展示或其他任何基本类型的唯一方式。不过对于结构体,println!
应该用来输出的格式是不明确的,因为这有更多显示的可能性:
-
是否需要 逗号
? -
需要打印出 大括号
吗? -
所有字段
都应该显示吗?
由于这种不确定性,Rust
不会尝试猜测我们的意图,所以结构体并没有提供一个 Display
实现。
不过,可以在 {}
中加入 :?
指示符告诉 println!
我们想要使用叫做 Debug
的输出格式。Debug
是一个 trait
,「它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值」。
Rust
确实包含了打印出调试信息的功能,不过我们「必须为结构体显式选择这个功能」。为此,在结构体定义之前
加上外部属性 #[derive(Debug)]
。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 结构为 {:?}", rect1);
}
答应结果为

为了能够「更好的显示数据结构」,此可以使用 {:#?}
替换 println!
字符串中的 {:?}
。如果在这个例子中使用了 {:#?}
风格的话,输出会看起来像这样

另一种使用 Debug
格式打印数值的方法是使用 dbg! 宏
。dbg! 宏
接收一个表达式的所有权
,「打印出代码中调用 dbg! 宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权」。调用 dbg! 宏
会打印到标准错误控制台流
(stderr
),而不是 println!
,后者会打印到标准输出控制台流
(stdout
)。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
输出结果为
方法语法
「方法」与函数类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
不过「方法与函数是不同的」,因为它们在结构体的上下文中被定义,并且它们「第一个参数总是 self
,它代表调用该方法的结构体实例」。
定义方法
实现一个定义于 Rectangle
结构体上的 area
方法。
#[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()
);
}
为了使函数定义于 Rectangle
的「上下文」中,使用 impl
块(impl
是 implementation
的缩写),「这个 impl
块中的所有内容都将与 Rectangle 类型相关联」。
接着将 area
函数移动到 impl
大括号中,并将签名中的第一个参数和函数体中其他地方的对应参数改成 self
。然后在 main
改成使用方法语法在 Rectangle
实例上调用 area
方法。方法语法
获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。
❝在一个
❞impl
块中,Self
类型是impl
块的「类型的别名」。方法的第一个参数「必须」有一个名为self
的Self
类型的参数,所以Rust
让你在第一个参数位置上只用self
这个名字来缩写。
使用方法替代函数,除了可使用方法语法和不需要在每个函数签名中重复 self
的类型之外,其主要好处在于「组织性」。
Rust
有一个叫 自动引用和解引用的功能。方法调用是 Rust
中少数几个拥有这种行为的地方。
❝它是这样工作的:当使用
❞object.something()
调用方法时,Rust
会自动为object
添加&
、&mut
或*
以便使object
与方法签名匹配。
也就是说,这些代码是等价的:
rect1.area()
(&rect1).area();
带有更多参数的方法
让一个 Rectangle
的实例获取另一个 Rectangle
实例,如果 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
}
}
在方法签名中,可以在 self
后增加多个参数,而且「这些参数就像函数中的参数一样工作」。
关联函数
❝所有在
❞impl
块中定义的函数被称为关联函数,因为它们与impl
后面命名的类型相关。我们可以定义不以self
为第一参数的关联函数(因此「不是方法」),因为它们并不作用于一个结构体的实例。
关联函数
经常被用作「返回一个结构体新实例的构造函数」。
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
使用结构体名和 :: 语法
来调用这个关联函数
:比如 let sq = Rectangle::square(3);
。这个「方法位于结构体的命名空间中」::: 语法
用于关联函数和模块创建的命名空间。
多个 impl 块
每个结构体都允许拥有多个 impl
块。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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
}
}
后记
「分享是一种态度」。
参考资料:《Rust权威指南》
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」

作者介绍

前端小魔女
微信公众号:前端柒八九