定义和实例化结构体
结构体类似于“元组类型”一节中讨论的元组,因为两者都包含多个相关的值。像元组一样,结构体中的各个部分可以是不同的类型。但与元组不同的是,在结构体中你需要为每个数据部分命名,以便明确这些值的含义。添加这些名称使得结构体比元组更灵活:你不必依赖数据的顺序来指定或访问实例中的值。
要定义一个结构体,我们输入关键字 struct 并为整个结构体命名。结构体的名称应描述被组合在一起的数据部分的重要性。然后,在花括号内定义数据部分(称为字段)的名称和类型。例如,清单 5-1 展示了一个存储用户账户信息的结构体。
文件名:src/main.rs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
清单 5-1:User 结构体定义
在定义好一个结构体后,要使用它,我们通过为每个字段指定具体值来创建该结构体的实例。我们通过写出该结构体名称,然后加上包含键值对(key: value)的花括号来创建实例,其中键是字段名,值是我们想存储在这些字段中的数据。我们不必按照声明时的顺序指定字段。换句话说,结构体验证就像一种通用模板,而实例则用特定的数据填充这个模板,从而创建该类型的具体数值。例如,我们可以如清单 5-2 所示声明一个特定用户。
文件名: src/main.rs
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");
}
清单 5-3:修改 User 实例中 email 字段的值
注意,整个实例必须是可变的;Rust 不允许我们只标记某些字段为可变。与任何表达式一样,我们可以在函数体的最后一个表达式中构造结构体的新实例,以隐式返回该新实例。
清单 5-4 展示了一个 build_user 函数,它返回一个带有给定 email 和 username 的 User 实例。active 字段被赋值为 true,sign_in_count 被赋值为 1。
文件名: src/main.rs
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
清单 5-4:一个接受 email 和 username 并返回 User 实例的 build_user 函数
将函数参数命名为与结构体字段相同的名字是合理的,但重复写出 email 和 username 字段名称及变量有点繁琐。如果结构体有更多字段,重复每个名字会更加烦人。幸运的是,有一种方便的简写方式!
使用字段初始化简写
因为参数名和结构体字段名在清单 5-4 中完全相同,我们可以使用字段初始化简写语法重写 build_user,使其行为完全相同,但不必重复书写 username 和 email,如清单 5-5 所示。
文件名:src/main.rs
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
清单 5-5:一个使用字段初始化简写的 build_user 函数,因为 username 和 email 参数与结构体字段同名
这里我们创建了一个新的 User 实例,它有一个名为 email 的字段。我们想将该 email 字段的值设置为 build_user 函数中 email 参数的值。由于 email 字段和参数名称相同,我们只需写成 email
,而不是 email: email
。
用结构体更新语法从其他实例创建新实例
通常需要基于另一个实例的大部分值来创建新实例,但修改其中一些值。这时可以使用结构体更新语法。
首先,在清单 5-6 中展示了如何常规地(不使用更新语法)创建 user2 新的 User 实例。我们给 email 设置了新值,其余则沿用了之前在清单 5-2 创建的 user1 的对应值。
文件名:src/main.rs
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,
};
}
清单 5-6:用除了一个之外全部来自 user1 值的新建 User 实例
利用结构体更新语法,可以用更少代码实现相同效果,如清单 5-7 所示。..
表示未显式设置的剩余字段应取自指定实例中的对应字段。
文件名:src/main.rs
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
清单 5-7:用结构体更新语法为 User 实例设定新的邮箱,同时其它所有值都来自于 user1
如上所示,user2 拥有不同的邮箱,但用户名、active 和 sign_in_count 等其它字段均继承自 user1。..user1
必须放最后,用以指明剩余未赋值的字段从哪个实例获取。但你可按任意顺序指定任意数量的具体字段,无需遵循结构定义中各个域出现顺序。
注意,结构体更新语法看起来像赋值操作符 =
;这是因为它会移动数据,就像“变量与数据交互中的移动”章节讲到的一样。在本例中,由于用户名字符串被移入到了 new 用户,所以不能再继续使用原来的 user1
。如果给 user2
提供了新的字符串类型邮箱和用户名,只复用了 active
与 sign_in_count
两个实现 Copy trait 的类型,那么在构造完后仍然能继续有效地访问原来的 user1
。这两个类型属于栈上复制的数据,因此适用于“仅限栈上的数据拷贝”章节讨论过的方法。本例还能访问到原来用户里的邮件地址,因为它没有被移动出去。
无命名域元组结构用于区分不同类型
Rust 同样支持类似元组但带名字标识符号称作元组结构(tuple structs)。元组结构拥有额外含义,即由其名字提供,但其内部元素没有独立命名,仅包含元素类型。当你希望整体给这个元组起个名字,并使得此类元组区别于其他普通元组时非常有用,也适合当每个成员逐一定义名称显得冗长或多余时采用。
定义方式是先声明 struct,再跟上名称及括号内列出的各元素类型。例如,这里定义并且使用两个分别叫 Color 和 Point 的元组结构:
文件名:src/main.rs
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 是不同类型,因为它们分别是不同元组结 构(struct) 类型下生成出来的对象。每种 struct 都是独立的新类型,即便里面成员都是一样的数据型别。例如,一个函数若要求传入 Color 类型,则不能传入 Point 类型作为实参,即使两者都由三个 i32 数字组成。此外,虽然 tuple struct 在解构拆包以及通过索引点位访问方面类似普通 tuple,不同的是解构时必须明确指出要解构的是哪种 tuple struct,比如要这样写:
let Point(x, y, z) = point;
无任何字段的类单元(struct Unit-Like Structs)
你还可以定义不包含任何字段的 struct!这些被称作类单元素(struct unit-like structs),因为它们行为类似于 () 单位值(在“Tuple Type”章节中提到过)。当你需要对某种类型实现 trait,但不需要在该类型本身存储任何数据时,类单元素很有用。我们将在第10章讨论 trait。这是声明和实例化一个名为 AlwaysEqual 的类单元素示例:
文件名:src/main.rs
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
定义 AlwaysEqual 时,我们使用了 struct 关键字、所需名称,然后以分号结尾。不需要花括号或圆括号!然后我们就能像这样获取 AlwaysEqual 实例:直接用已定义好的名字,不带花括号或圆括号。假设以后我们会实现这个类型的一些行为,使得所有 AlwaysEqual 实例总是等价于任意其他实例,比如用于测试目的且结果可预知。这种行为无需存储任何数据即可实现!第10章将介绍如何定义 trait 并将其应用到包括类单元素在内任意类型上。
Struct 数据所有权
在清单5-1中的 User struct 定义里,我们用了拥有所有权(owned)的 String 类型,而非字符串切片 &str。这是刻意选择,因为希望每个该 struct 实例拥有自身全部数据,并保证这些数据只要整个 struct 有效就一直有效。
当然,也可以让 structs 存储指向其它地方拥有的数据引用,但这要求使用生命周期(lifetimes),这是 Rust 的一项特性,第10章会详细讲解。生命周期确保被引用的数据只要对应 struct 有效就保持有效。如果尝试在未指定生命周期情况下,将引用存入 struct,如下代码,则无法编译:
文件名:src/main.rs
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,
};
}
编译器会报错提示缺少生命周期说明符:
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username:&str,
4 ~ email:&'a str,
For more information about this error, try `rustc --explain E0106`.
error : could not compile `structs` (bin "structs") due to previous errors.
第10章中我们将讲述如何修复此错误,以便能够把引用存入 structs;但目前,我们通过改用拥有所有权的数据如 String 来替代引用(&str),从而避免此类错误。