参考:https://kaisery.github.io/trpl-zh-cn/ch05-01-defining-structs.html

结构体 (struct)

struct 描述

一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。strcut 的类型就是 struct 本身。
stuct 的名称应该是 UpperCamelCase
在面向对象语言中,struct 就像对象中的数据属性。
元组与结构体的异同:

  • 同:和元组一样,结构体的每一部分可以是不同类型
  • 异:结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活,因为不需要依赖顺序来指定或访问实例中的值。

结构体的目的:让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。
方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。

定义 struct

  1. 常规的 struct:

    1. struct 结构体名称 {
    2. 字段1名称(无引号): 字段1类型,
    3. ...
    4. }
  2. 元组结构体 tuple structs:
    元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
    创建实例只需要按顺序填写符合类型的值。
    当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了,例如 颜色、坐标。另一种常见的情形是元组结构体 “作为枚举体的成员” 。也有一种场景很实用:”newtype 模式” 。
    即使 A 和 B 两个结构体中的字段有着完全相同的类型,一个获取 A 结构体作为参数的函数无法接收 B 结构体参数,因为 A 与 B 是不同的类型。
    元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 . 后跟索引来访问单独的值,如针对下面的例子,使用 Color.0 获取该结构体第一个字段的值。

    1. // 定义 tuple structs
    2. struct Color(u8, u8, u8); // RGB 色彩模式
    3. struct Point(i32, i32, i32); // 三维点坐标
    4. // 创建实例
    5. let color = Color(0, 0, 0) // 表示 黑色
    6. let origin = Point(0, 0, 0) // 表示 原点
  3. 类单元结构体 unit-like structs:没有任何字段的 struct,它们类似于 “unit 类型” () 。unit-like structs 常常在你想要在某个类型上实现 trait/泛型 但不需要在类型中存储数据的时候发挥作用。

    1. struct NoName {}
    2. struct Nil;
    3. struct PhantomData<T: ?Size>;

结构体粗略地看只有 {}() 两种符号表示:

  • {} 内部的字段有名称,各字段之间无序(不在乎顺序,因为只要有名字就够了);
  • () 内部没有字段名称,位置与固定的类型一一对应(必须有顺序,因为没名字)。

    创建实例:针对常规的 struct

    一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例
    实例中字段的顺序不需要和它们在结构体中声明的顺序一致。
    从结构体中获取某个特定的值,可以使用点号:结构体名称.字段名称
    创建方式:let 实例变量名 = 结构体名称 {字段}
  1. 普通方式创建实例:以结构体的名字开头,接着在大括号中使用 key: value 键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。
  2. 函数返回时创建实例:在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
    字段初始化简写语法(field init shorthand):函数参数与结构体字段同名时只需要写 key。
  3. 从其他存在的实例创建实例(结构体更新语法 struct update syntax):使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例。两种写法:
    采用 实例名.字段名 的方式:明确地指定;
    采用 ..实例名 方式:指定剩余未显式设置值的字段应有与给定实例对应字段相同的值。
    注意:不具备 Copy trait 的数据的值会被 move 掉。
  4. 关联函数 (associated function) 创建实例:只需要输入简单的形式,通过函数最终处理成结构体需要的字段信息。比如 xx::new()xx::from()xx::parse()

创建可变实例:在实例变量名称加 mut 关键字,即 let mut 实例变量名 = 结构体名称 {字段}

  • 要更改结构体中的值,则实例必须是可变的,然后使用点号并为对应的字段赋值。
  • 注意整个实例必须是可变的:Rust 并不允许只将某个字段标记为可变。

    1. fn main() {
    2. // 定义结构体,使用 Debug trait 用来打印
    3. #[derive(Debug)]
    4. struct User {
    5. // String 是拥有所有权且具有 move 语义的类型,结构体拥有它所有的数据,数据会被转交(move)出去
    6. username: String,
    7. account: String,
    8. // u64 是拥有所有权且具有 copy 语义的类型,结构体拥有它所有的数据,数据传递的时候会 copy 一份出去
    9. sign_in_count: u64,
    10. } // 注意没有分号
    11. // 实例化结构体,字段的顺序可以改变
    12. let user1 = User {
    13. account: "001".to_string(),
    14. sign_in_count: 1,
    15. username: String::from("User1"),
    16. };
    17. // 创建可变实例:实例的值可以被改变
    18. let mut user2 = User {
    19. account: "002".to_string(),
    20. sign_in_count: 2,
    21. username: String::from("User1"),
    22. };
    23. user2.username = String::from("User2");
    24. // 字段初始化简写语法 field init shorthand
    25. // 在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例
    26. fn build_user(username: String, account: String) -> User {
    27. User {
    28. // 字段初始化简写语法(field init shorthand):函数参数与结构体字段同名时只需要 key
    29. // 无需 username: username,
    30. username,
    31. // 无需 account: account,
    32. account,
    33. sign_in_count: 0,
    34. }
    35. }
    36. let user3 = build_user(String::from("User3"), String::from("003"));
    37. println!(
    38. "nomarl instantiation: {:#?}\nmutable instance: {:#?}\ninstantiated from function return value: {:#?}",
    39. user1, user2, user3
    40. );
    41. // 结构体更新语法 struct update syntax
    42. // 使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例
    43. // 注意:不具备 Copy trait 的数据的值会被 move 掉
    44. // 采用 `实例名.字段名` 的方式:明确地指定
    45. let user1_explicit = User {
    46. username: user1.username, // user1.username 的值被 moved,无法再使用
    47. account: "004".to_string(), // user1.account 的值被 moved,无法再使用
    48. sign_in_count: 5,
    49. };
    50. // 采用 `..实例名` 方式:指定剩余未显式设置值的字段应有与给定实例对应字段相同的值
    51. let user2_implicit = User {
    52. account: "005".to_string(),
    53. ..user2 // user2 除 account 之外字段且不具备 Copy trait 的值被 moved;注意不需要逗号或分号
    54. };
    55. // 由于 u64 具有 Copy trait,不会被 moved
    56. println!("user2.sign_in_count: {}", user2.sign_in_count);
    57. println!(
    58. "struct update syntax:\n{:#?}\n{:#?}",
    59. user1_explicit, user2_implicit
    60. );
    61. println!("模拟了 3 个用户名、五个帐号的登录情况,以上所有 let (mut) 声明的变量的类型都是 User");
    62. }

    打印结果:

    1. nomarl instantiation: User {
    2. username: "User1",
    3. account: "001",
    4. sign_in_count: 1,
    5. }
    6. mutable instance: User {
    7. username: "User2",
    8. account: "002",
    9. sign_in_count: 2,
    10. }
    11. instantiated from function return value: User {
    12. username: "User3",
    13. account: "003",
    14. sign_in_count: 0,
    15. }
    16. user2.sign_in_count: 2
    17. struct update syntax:
    18. User {
    19. username: "User1",
    20. account: "004",
    21. sign_in_count: 5,
    22. }
    23. User {
    24. username: "User2",
    25. account: "005",
    26. sign_in_count: 2,
    27. }
    28. 模拟了 3 个用户名、五个帐号的登录情况,以上所有 let (mut) 声明的变量的类型都是 User

    结构体数据的所有权

    注意上面例子中 struct 使用的数据类型让 struct 拥有数据的所有权,那么创建的实例数据如果是 move 语义就可能被转移出去。
    struct 也可以存储被其他对象拥有的数据的引用不过这么做的话需要用上 生命周期lifetimes ),这是一个第十章会讨论的 Rust 功能。#todo# 生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的。无效的例子见 Rust Book:结构体数据的所有权

    方法 (method)

    定义和调用方法的例子:计算矩形面积、比较两个矩形大小

    1. #[derive(Debug)]
    2. struct Rectangle {
    3. width: u32,
    4. height: u32,
    5. }
    6. impl Rectangle {
    7. // 带有自身数据的方法
    8. fn area(&self) -> u32 {
    9. self.width * self.height
    10. }
    11. // 带有外部数据的方法
    12. fn can_hold(&self, other: &Rectangle) -> bool {
    13. self.width > other.width && self.height > other.height
    14. }
    15. }
    16. fn main() {
    17. let rect1 = Rectangle { width: 30, height: 50 };
    18. println!(
    19. "The area of the rectangle is {} square pixels.",
    20. rect1.area() // 用 `.` 调用 area 方法
    21. );
    22. let rect2 = Rectangle { width: 10, height: 40 };
    23. println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); // 用 `.` 调用 can_hold 方法
    24. }

    方法 相较于函数:

  • 相同:方法使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。

  • 不同:方法在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是 &self 之类的,它代表调用该方法的结构体实例。

&selfself: &Self 的语法糖(sugar),其中 Self 是方法调用者的 类型;同理 &mut selfself: &mut Self 的语法糖;selfself: Self 的语法糖。
方法第一个参数使用 self 还是 &self 还是 &mut self

  • 使用 &self 的理由:并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。在大部分场景下使用。
  • 使用 &mut self 的理由:在方法中改变调用方法的实例
  • 使用 self 的理由:使方法获取实例的所有权,这种情况是很少见的,通常用在当方法将 self 转换成别的实例时,为了防止调用者在转换之后使用原始的实例

隐式借用:
在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。即如果 object 是一个指针,那么应该使用 object->something() 或者 (*object).something() 来调用方法。
但是在 Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用automatic referencing and dereferencing )的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。这个功能工作方式:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut* 以便使 object 与方法签名匹配。

  1. // impl 块中的方法
  2. fn area(&self) -> u32 {
  3. self.width * self.height
  4. }
  5. // 实例调用方法:自动引用和解引用 (automatic referencing and dereferencing)
  6. rect1.area() // 等价于 (&rect1).area()
  7. // 如果是方法第一个参数是 &mut self,那么调用时等价于 (&mut rect1).area()

rect1.area() 看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
注意,如果单独使用 object,比如在赋值的时候 let a = object,并不具备这种自动机制,因为 object 是 不可变引用、可变引用、值的时候,a 也是相应的类型,不会被改变。

关联函数 (associated functions)

关联函数 associated functions:在结构体内定义的 self 作为参数的函数。有时 关联函数 被称作静态方法 (static method)。
关联函数将特定功能置于结构体的命名空间中并且无需一个实例。关联函数仍是函数而不是方法,因为它们并不作用于一个结构体的实例。
关联函数经常被用作返回一个结构体新实例的构造函数 (constructor),比如 String::from
例子:构造正方形

  1. #[derive(Debug)]
  2. struct Rectangle {
  3. width: u32,
  4. height: u32,
  5. }
  6. impl Rectangle {
  7. fn square(size: u32) -> Rectangle {
  8. Rectangle { width: size, height: size }
  9. }
  10. }
  11. fn main() {
  12. let sq = Rectangle::square(3);
  13. println!("{:#?}", sq);
  14. }

impl 块

每个结构体都允许拥有多个 impl 块,这样就可以把各个方法或关联函数拆开写。

  1. // 把同一个结构体的方法或者关联函数拆开单独放在 impl 块
  2. impl Rectangle {
  3. // 带有自身数据的方法
  4. fn area(&self) -> u32 {
  5. self.width * self.height
  6. }
  7. }
  8. impl Rectangle {
  9. // 带有外部数据的方法
  10. fn can_hold(&self, other: &Rectangle) -> bool {
  11. self.width > other.width && self.height > other.height
  12. }
  13. }
  14. impl Rectangle {
  15. // 关联函数
  16. fn square(size: u32) -> Rectangle {
  17. Rectangle { width: size, height: size }
  18. }
  19. }

这里没有理由将这些方法分散在多个 impl 块中,不过这是有效的语法。第十章讨论泛型和 trait 时会看到实用的多 impl 块的用例。#todo#