参考: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 structs
struct 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,不会被 moved
println!("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: 2
struct 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#