结构体的定义和实例化

ch05-01-defining-structs.md
commit dd7e05275822d6cf790bcdae6983b3234141b5e7

结构体和我们在“元组类型”部分论过的元组类似,它们都包含多个相关的值。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。

定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段field)。例如,示例 5-1 展示了一个存储用户账号信息的结构体:

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn main() {}

示例 5-1:User 结构体定义

一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value 键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户:

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn main() {
  8. let user1 = User {
  9. email: String::from("someone@example.com"),
  10. username: String::from("someusername123"),
  11. active: true,
  12. sign_in_count: 1,
  13. };
  14. }

示例 5-2:创建 User 结构体的实例

为了从结构体中获取某个特定的值,可以使用点号。举个例子,想要用户的邮箱地址,可以用 user1.email。如果结构体的实例是可变的,我们可以使用点号并为对应的字段赋值。示例 5-3 展示了如何改变一个可变的 User 实例中 email 字段的值:

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn main() {
  8. let mut user1 = User {
  9. email: String::from("someone@example.com"),
  10. username: String::from("someusername123"),
  11. active: true,
  12. sign_in_count: 1,
  13. };
  14. user1.email = String::from("anotheremail@example.com");
  15. }

示例 5-3:改变 User 实例 email 字段的值

注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。

示例 5-4 显示了一个 build_user 函数,它返回一个带有给定的 email 和用户名的 User 结构体实例。active 字段的值为 true,并且 sign_in_count 的值为 1

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn build_user(email: String, username: String) -> User {
  8. User {
  9. email: email,
  10. username: username,
  11. active: true,
  12. sign_in_count: 1,
  13. }
  14. }
  15. fn main() {
  16. let user1 = build_user(
  17. String::from("someone@example.com"),
  18. String::from("someusername123"),
  19. );
  20. }

示例 5-4:build_user 函数获取 email 和用户名并返回 User 实例

为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 emailusername 字段名称与变量有些啰嗦。如果结构体有更多字段,重复每个名称就更加烦人了。幸运的是,有一个方便的简写语法!

使用字段初始化简写语法

因为示例 5-4 中的参数名与字段名都完全相同,我们可以使用 字段初始化简写语法field init shorthand)来重写 build_user,这样其行为与之前完全相同,不过无需重复 emailusername 了,如示例 5-5 所示。

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn build_user(email: String, username: String) -> User {
  8. User {
  9. email,
  10. username,
  11. active: true,
  12. sign_in_count: 1,
  13. }
  14. }
  15. fn main() {
  16. let user1 = build_user(
  17. String::from("someone@example.com"),
  18. String::from("someusername123"),
  19. );
  20. }

示例 5-5:build_user 函数使用了字段初始化简写语法,因为 emailusername 参数与结构体字段同名

这里我们创建了一个新的 User 结构体实例,它有一个叫做 email 的字段。我们想要将 email 字段的值设置为 build_user 函数 email 参数的值。因为 email 字段与 email 参数有着相同的名称,则只需编写 email 而不是 email: email

使用结构体更新语法从其他实例创建实例

使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有用的。这可以通过 结构体更新语法struct update syntax)实现。

首先,示例 5-6 展示了不使用更新语法时,如何在 user2 中创建一个新 User 实例。我们为 email 设置了新的值,其他值则使用了实例 5-2 中创建的 user1 中的同名值:

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn main() {
  8. // --snip--
  9. let user1 = User {
  10. email: String::from("someone@example.com"),
  11. username: String::from("someusername123"),
  12. active: true,
  13. sign_in_count: 1,
  14. };
  15. let user2 = User {
  16. active: user1.active,
  17. username: user1.username,
  18. email: String::from("another@example.com"),
  19. sign_in_count: user1.sign_in_count,
  20. };
  21. }

示例 5-6:使用 user1 中的一个值创建一个新的 User 实例

使用结构体更新语法,我们可以通过更少的代码来达到相同的效果,如示例 5-7 所示。.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。

  1. struct User {
  2. active: bool,
  3. username: String,
  4. email: String,
  5. sign_in_count: u64,
  6. }
  7. fn main() {
  8. // --snip--
  9. let user1 = User {
  10. email: String::from("someone@example.com"),
  11. username: String::from("someusername123"),
  12. active: true,
  13. sign_in_count: 1,
  14. };
  15. let user2 = User {
  16. email: String::from("another@example.com"),
  17. ..user1
  18. };
  19. }

示例 5-7:使用结构体更新语法为一个 User 实例设置一个新的 email 值,不过其余值来自 user1 变量中实例的字段

示例 5-7 中的代码也在 user2 中创建了一个新实例,但该实例中 email 字段的值与 user1 不同,而 usernameactivesign_in_count 字段的值与 user1 相同。..user1 必须放在最后,以指定其余的字段应从 user1 的相应字段中获取其值,但我们可以选择以任何顺序为任意字段指定值,而不用考虑结构体定义中字段的顺序。

请注意,结构更新语法就像带有 = 的赋值,因为它移动了数据,就像我们在“变量与数据交互的方式(一):移动”部分讲到的一样。在这个例子中,我们在创建 user2 后不能再使用 user1,因为 user1username 字段中的 String 被移到 user2 中。如果我们给 user2emailusername 都赋予新的 String 值,从而只使用 user1activesign_in_count 值,那么 user1 在创建 user2 后仍然有效。activesign_in_count 的类型是实现 Copy trait 的类型,所以我们在“变量与数据交互的方式(二):克隆” 部分讨论的行为同样适用。

使用没有命名字段的元组结构体来创建不同的类型

也可以定义与元组(在第三章讨论过)类似的结构体,称为 元组结构体tuple structs)。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。

要定义元组结构体,以 struct 关键字和结构体名开头并后跟元组中的类型。例如,下面是两个分别叫做 ColorPoint 元组结构体的定义和用法:

  1. struct Color(i32, i32, i32);
  2. struct Point(i32, i32, i32);
  3. fn main() {
  4. let black = Color(0, 0, 0);
  5. let origin = Point(0, 0, 0);
  6. }

注意 blackorigin 值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段可能有着相同的类型。例如,一个获取 Color 类型参数的函数不能接受 Point 作为参数,即便这两个类型都由三个 i32 值组成。在其他方面,元组结构体实例类似于元组,你可以将它们解构为单独的部分,也可以使用 . 后跟索引来访问单独的值,等等。

没有任何字段的类单元结构体

我们也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体unit-like structs)因为它们类似于 (),即“元组类型”一节中提到的 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。我们将在第十章介绍 trait。下面是一个声明和实例化一个名为 AlwaysEqual 的 unit 结构的例子。

  1. struct AlwaysEqual;
  2. fn main() {
  3. let subject = AlwaysEqual;
  4. }

要定义 AlwaysEqual,我们使用 struct 关键字,我们想要的名称,然后是一个分号。不需要花括号或圆括号!然后,我们可以以类似的方式在 subject 变量中获得 AlwaysEqual 的实例:使用我们定义的名称,不需要任何花括号或圆括号。想象一下,我们将实现这个类型的行为,即每个实例始终等于每一个其他类型的实例,也许是为了获得一个已知的结果以便进行测试。我们不需要任何数据来实现这种行为,你将在第十章中,看到如何定义特性并在任何类型上实现它们,包括类单元结构体。

结构体数据的所有权

在示例 5-1 中的 User 结构体的定义中,我们使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。

可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期lifetimes),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的,比如这样:

文件名: src/main.rs

  1. struct User {
  2. active: bool,
  3. username: &str,
  4. email: &str,
  5. sign_in_count: u64,
  6. }
  7. fn main() {
  8. let user1 = User {
  9. email: "someone@example.com",
  10. username: "someusername123",
  11. active: true,
  12. sign_in_count: 1,
  13. };
  14. }

编译器会抱怨它需要生命周期标识符:

  1. $ cargo run
  2. Compiling structs v0.1.0 (file:///projects/structs)
  3. error[E0106]: missing lifetime specifier
  4. —> src/main.rs:3:15
  5. |
  6. 3 | username: &str,
  7. | ^ expected named lifetime parameter
  8. |
  9. help: consider introducing a named lifetime parameter
  10. |
  11. 1 ~ struct User<'a> {
  12. 2 | active: bool,
  13. 3 ~ username: &'a str,
  14. |
  15. error[E0106]: missing lifetime specifier
  16. —> src/main.rs:4:12
  17. |
  18. 4 | email: &str,
  19. | ^ expected named lifetime parameter
  20. |
  21. help: consider introducing a named lifetime parameter
  22. |
  23. 1 ~ struct User<'a> {
  24. 2 | active: bool,
  25. 3 | username: &str,
  26. 4 ~ email: &'a str,
  27. |
  28. For more information about this error, try rustc --explain E0106.
  29. error: could not compile structs due to 2 previous errors

第十章会讲到如何修复这个问题以便在结构体中存储引用,不过现在,我们会使用像 String 这类拥有所有权的类型来替代 &str 这样的引用以修正这个错误。