Rust 使用结构体组织相关联的数据
本文有删减,原文请参考使用结构体组织相关联的数据。
struct 或者 structure 是一个自定义数据类型,允许你包装和命名多个相关的值,从而形成一个有意义的组合。
结构体的定义和实例化
和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。
由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
定义结构体的语法如下。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
注意:在大括号中定义的名字和类型称为 字段(field)。
结构体实例的创建和可变赋值如下:
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");
}
注意:整个实例必须是可变的,Rust 并不允许只将某个字段标记为可变。
同其他任何表达式一样,可以在函数体的最后一个表达式中构造一个结构体的新实例来隐式地返回这个实例:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
使用字段初始化简写语法
上面示例中的参数名与字段名都完全相同,可以使用 字段初始化简写语法(field init shorthand) 来重写 build_user:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
build_user 函数使用了字段初始化简写语法,因为 username 和 email 参数与结构体字段同名,只需编写 email 而不是 email: email。
使用结构体更新语法从其他实例创建实例
使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有用的,这可以通过 结构体更新语法(struct update syntax) 实现。
使用结构体更新语法为一个 User 实例设置一个新的 email 值,不过其余值来自 user1 变量中实例的字段:
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
//..user1 必须放在最后,以指定其余的字段应从 user1 的相应字段中获取其值
..user1
};
}
使用结构体更新语法可以通过更少的代码来达到目的,..语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值:
请注意,结构更新语法就像带有 = 的赋值,因为它移动了数据。总体上说,我们在创建 user2 后不能就再使用 user1 了,因为 user1 的 username 字段中的 String 被移到 user2 中。如果我们给 user2 的 email 和 username 都赋予新的 String 值,从而只使用 user1 的 active 和 sign_in_count 值,那么 user1 在创建 user2 后仍然有效。active 和 sign_in_count 的类型是实现 Copy trait 的类型。
使用没有命名字段的元组结构体来创建不同的类型
可以定义与元组类似的结构体,称为 元组结构体(tuple structs)。元组结构体有着结构体名称提供的含义,但没有具体的字段名只有字段的类型。
当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的。
要定义元组结构体,以 struct 关键字和结构体名开头并后跟元组中的类型:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
//black 和 origin 值的类型不同,因为它们是不同的元组结构体的实例
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
一个获取 Color 类型参数的函数不能接受 Point 作为参数,即便这两个类型都由三个 i32 值组成。在其他方面,元组结构体实例类似于元组,可以将它们解构为单独的部分,也可以使用 . 后跟索引来访问单独的值。
没有任何字段的类单元结构体
可以定义一个没有任何字段的结构体,它们被称为 类单元结构体(unit-like structs) ,因为它们类似于单元元组。
下面是一个声明和实例化一个名为 AlwaysEqual 的 unit 结构的例子:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
在 subject 变量中创建 AlwaysEqual 的实例:只需使用定义的名称,无需任何花括号或圆括号。
结构体示例程序
编写一个计算长方形面积的程序:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
通过派生 trait 增加实用功能
在调试程序时打印出 Rectangle 实例来查看其所有字段的值非常有用,但并不能像之前一样使用 println! 宏。
println! 宏能处理很多类型的格式,不过,{} 默认告诉 println! 使用被称为 Display 的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display,因为它就是向用户展示 1 或其他任何基本类型的唯一方式。不过对于结构体,println! 应该用来输出的格式是不明确的,所以结构体并没有提供一个 Display 实现来使用 println! 与 {} 占位符。
需要在 {} 中加入 :? 指示符告诉 println! 要使用 Debug 输出格式,并且在结构体定义之前加上外部属性 #[derive(Debug)] 为结构体显式选择这个功能:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
//{:?}适用于快速查看变量的值
println!("rect1 is {:?}", rect1);
//{:#?}适用于更详细的调试输出
println!("rect1 is {:#?}", rect1);
}
Debug 是一个 trait,它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值。
输出结果:
rect1 is Rectangle { width: 30, height: 50 }
rect1 is Rectangle {
width: 30,
height: 50,
}
另一种使用 Debug 格式打印数值的方法是使用 dbg! 宏,dbg! 宏接收一个表达式的所有权(与 println! 宏相反,后者接收的是引用),打印出代码中调用 dbg! 宏时所在的文件和行号以及该表达式的结果值,并返回该值的所有权。
打印出分配给 width 字段的值以及 rect1 中整个结构的值:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
输出结果:
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
注意:调用 dbg! 宏会打印到标准错误控制台流(stderr),与 println! 不同,后者会打印到标准输出控制台流(stdout)。
方法语法
方法(method)与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
不过方法与函数是不同的是:
- 方法在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文)
- 方法第一个参数总是 self,它代表调用该方法的结构体实例
定义方法
在 Rectangle 结构体上定义 area 方法:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
//impl 块(impl 是 implementation 的缩写)
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
//在 Rectangle 实例上调用 area 方法
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
在 area 的签名中,&self 实际上是 self: &Self 的缩写。在一个 impl 块中,Self 类型是 impl 块的类型的别名。方法的第一个参数必须有一个名为 self 的Self 类型的参数,所以 Rust 让你在第一个参数位置上只用 self 这个名字来缩写。
这里选择 &self 只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self。通过仅仅使用 self 作为第一个参数来使方法获取实例的所有权是很少见的,这种技术通常用在当方法将 self 转换成别的实例的时候。
可以选择将方法的名称与结构中的一个字段相同,但通常与字段同名的方法将被定义为只返回字段中的值,而不做其他事情。这样的方法被称为 getters,Rust 并不像其他一些语言那样为结构字段自动实现它们。
-> 运算符到哪去了?
在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用(automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。
它是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:
p1.distance(&p2);
(&p1).distance(&p2);第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
带有更多参数的方法
让一个 Rectangle 的实例获取另一个 Rectangle 实例,如果 self (第一个 Rectangle)能完全包含第二个长方形则返回 true;否则返回 false:
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
}
}
关联函数
所有在 impl 块中定义的函数被称为 *关联函数(associated functions),因为它们与 impl 后面命名的类型相关。
可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例,如在 String 类型上定义的 String::from 函数。
不是方法的关联函数经常被用作返回一个结构体新实例的构造函数,这些函数的名称通常为 new (new 并不是一个关键字)。
提供一个叫做 square 关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
关键字 Self 在函数的返回类型中代指在 impl 关键字后出现的类型,在这里是 Rectangle。
使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个函数位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。
多个 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
}
}