参考:https://kaisery.github.io/trpl-zh-cn/ch05-01-defining-structs.html
结构体 (struct)
struct 描述
一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。strcut 的类型就是 struct 本身。
stuct 的名称应该是 UpperCamelCase
在面向对象语言中,struct 就像对象中的数据属性。
元组与结构体的异同:
- 同:和元组一样,结构体的每一部分可以是不同类型
- 异:结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活,因为不需要依赖顺序来指定或访问实例中的值。
结构体的目的:让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。
方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。
定义 struct
常规的 struct:
struct 结构体名称 {字段1名称(无引号): 字段1类型,...}
元组结构体 tuple structs:
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。
创建实例只需要按顺序填写符合类型的值。
当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了,例如 颜色、坐标。另一种常见的情形是元组结构体 “作为枚举体的成员” 。也有一种场景很实用:”newtype 模式” 。
即使 A 和 B 两个结构体中的字段有着完全相同的类型,一个获取 A 结构体作为参数的函数无法接收 B 结构体参数,因为 A 与 B 是不同的类型。
元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用.后跟索引来访问单独的值,如针对下面的例子,使用Color.0获取该结构体第一个字段的值。// 定义 tuple structsstruct Color(u8, u8, u8); // RGB 色彩模式struct Point(i32, i32, i32); // 三维点坐标// 创建实例let color = Color(0, 0, 0) // 表示 黑色let origin = Point(0, 0, 0) // 表示 原点
类单元结构体 unit-like structs:没有任何字段的 struct,它们类似于 “unit 类型”
()。unit-like structs 常常在你想要在某个类型上实现 trait/泛型 但不需要在类型中存储数据的时候发挥作用。struct NoName {}struct Nil;struct PhantomData<T: ?Size>;
结构体粗略地看只有 {} 和 () 两种符号表示:
{}内部的字段有名称,各字段之间无序(不在乎顺序,因为只要有名字就够了);()内部没有字段名称,位置与固定的类型一一对应(必须有顺序,因为没名字)。创建实例:针对常规的 struct
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例 。
实例中字段的顺序不需要和它们在结构体中声明的顺序一致。
从结构体中获取某个特定的值,可以使用点号:结构体名称.字段名称。
创建方式:let 实例变量名 = 结构体名称 {字段}
- 普通方式创建实例:以结构体的名字开头,接着在大括号中使用
key: value键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。 - 函数返回时创建实例:在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
字段初始化简写语法(field init shorthand):函数参数与结构体字段同名时只需要写 key。 - 从其他存在的实例创建实例(结构体更新语法 struct update syntax):使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例。两种写法:
采用实例名.字段名的方式:明确地指定;
采用..实例名方式:指定剩余未显式设置值的字段应有与给定实例对应字段相同的值。
注意:不具备 Copy trait 的数据的值会被 move 掉。 - 关联函数 (associated function) 创建实例:只需要输入简单的形式,通过函数最终处理成结构体需要的字段信息。比如
xx::new()、xx::from()、xx::parse()。
创建可变实例:在实例变量名称加 mut 关键字,即 let mut 实例变量名 = 结构体名称 {字段}
- 要更改结构体中的值,则实例必须是可变的,然后使用点号并为对应的字段赋值。
注意整个实例必须是可变的:Rust 并不允许只将某个字段标记为可变。
fn main() {// 定义结构体,使用 Debug trait 用来打印#[derive(Debug)]struct User {// String 是拥有所有权且具有 move 语义的类型,结构体拥有它所有的数据,数据会被转交(move)出去username: String,account: String,// u64 是拥有所有权且具有 copy 语义的类型,结构体拥有它所有的数据,数据传递的时候会 copy 一份出去sign_in_count: u64,} // 注意没有分号// 实例化结构体,字段的顺序可以改变let user1 = User {account: "001".to_string(),sign_in_count: 1,username: String::from("User1"),};// 创建可变实例:实例的值可以被改变let mut user2 = User {account: "002".to_string(),sign_in_count: 2,username: String::from("User1"),};user2.username = String::from("User2");// 字段初始化简写语法 field init shorthand// 在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例fn build_user(username: String, account: String) -> User {User {// 字段初始化简写语法(field init shorthand):函数参数与结构体字段同名时只需要 key// 无需 username: username,username,// 无需 account: account,account,sign_in_count: 0,}}let user3 = build_user(String::from("User3"), String::from("003"));println!("nomarl instantiation: {:#?}\nmutable instance: {:#?}\ninstantiated from function return value: {:#?}",user1, user2, user3);// 结构体更新语法 struct update syntax// 使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例// 注意:不具备 Copy trait 的数据的值会被 move 掉// 采用 `实例名.字段名` 的方式:明确地指定let user1_explicit = User {username: user1.username, // user1.username 的值被 moved,无法再使用account: "004".to_string(), // user1.account 的值被 moved,无法再使用sign_in_count: 5,};// 采用 `..实例名` 方式:指定剩余未显式设置值的字段应有与给定实例对应字段相同的值let user2_implicit = User {account: "005".to_string(),..user2 // user2 除 account 之外字段且不具备 Copy trait 的值被 moved;注意不需要逗号或分号};// 由于 u64 具有 Copy trait,不会被 movedprintln!("user2.sign_in_count: {}", user2.sign_in_count);println!("struct update syntax:\n{:#?}\n{:#?}",user1_explicit, user2_implicit);println!("模拟了 3 个用户名、五个帐号的登录情况,以上所有 let (mut) 声明的变量的类型都是 User");}
打印结果:
nomarl instantiation: User {username: "User1",account: "001",sign_in_count: 1,}mutable instance: User {username: "User2",account: "002",sign_in_count: 2,}instantiated from function return value: User {username: "User3",account: "003",sign_in_count: 0,}user2.sign_in_count: 2struct update syntax:User {username: "User1",account: "004",sign_in_count: 5,}User {username: "User2",account: "005",sign_in_count: 2,}模拟了 3 个用户名、五个帐号的登录情况,以上所有 let (mut) 声明的变量的类型都是 User
结构体数据的所有权
注意上面例子中 struct 使用的数据类型让 struct 拥有数据的所有权,那么创建的实例数据如果是 move 语义就可能被转移出去。
struct 也可以存储被其他对象拥有的数据的引用不过这么做的话需要用上 生命周期 (lifetimes ),这是一个第十章会讨论的 Rust 功能。#todo# 生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的。无效的例子见 Rust Book:结构体数据的所有权方法 (method)
定义和调用方法的例子:计算矩形面积、比较两个矩形大小
#[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}}fn main() {let rect1 = Rectangle { width: 30, height: 50 };println!("The area of the rectangle is {} square pixels.",rect1.area() // 用 `.` 调用 area 方法);let rect2 = Rectangle { width: 10, height: 40 };println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); // 用 `.` 调用 can_hold 方法}
方法 相较于函数:
相同:方法使用
fn关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。- 不同:方法在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是
&self之类的,它代表调用该方法的结构体实例。
&self 是 self: &Self 的语法糖(sugar),其中 Self 是方法调用者的 类型;同理 &mut self 为 self: &mut Self 的语法糖;self 为 self: 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 与方法签名匹配。
// impl 块中的方法fn area(&self) -> u32 {self.width * self.height}// 实例调用方法:自动引用和解引用 (automatic referencing and dereferencing)rect1.area() // 等价于 (&rect1).area()// 如果是方法第一个参数是 &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 。
例子:构造正方形
#[derive(Debug)]struct Rectangle {width: u32,height: u32,}impl Rectangle {fn square(size: u32) -> Rectangle {Rectangle { width: size, height: size }}}fn main() {let sq = Rectangle::square(3);println!("{:#?}", sq);}
impl 块
每个结构体都允许拥有多个 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 Rectangle {// 关联函数fn square(size: u32) -> Rectangle {Rectangle { width: size, height: size }}}
这里没有理由将这些方法分散在多个 impl 块中,不过这是有效的语法。第十章讨论泛型和 trait 时会看到实用的多 impl 块的用例。#todo#
