Rust-结构体
Rust-结构体
结构体是一种可支持我们进行自定义的数据类型,它允许我们可以把多个相关联的值进行打包,组成一个有意义的组合,并取一个新的名字。
一、结构体语法
1. 如何定义一个结构体?
- 使用struct关键字,并为整个结构体进行命名。
- 在花括号内,为所有成员字段定义名称和类型。
- 每个成员字段以逗号进行分隔,即使是最后一个成员也需要如此。
例如, 以下结构体定义了某网站的用户:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,它是 String 类型。
2. 如何使用一个结构体?
想要使用结构体,就需要先创建结构体的实例:
-
为每个字段指定具体的值,不多不少。
-
无需按照声明的顺序进行指定
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 110,
};
3. 如何取得结构体里面的某个值?
通过.
操作符即可访问结构体实例内部的字段值,也可以修改它们(修改要在创建结构体实例时使用mut
):
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com"); //赋新值
注意:一旦 struct 的实例是可变的(mut
),那么实例中所有的字段都是可变的。Rust 不支持将结构体中的特定字段标记为可变而其他字段却不可变。
4. 简化结构体创建
简化结构体创建的操作用到了函数,这利用了“struct也可作为函数的返回值”的特性。
以下面的函数为例,它接收两个字符串参数: email 和 username,然后使用它们来创建一个 User 结构体,并且返回了 User 结构体的实例:
fn build_user(email:String, username:String) -> User{
User{
emali: email,
username: username,
active: true,
sign_in_count: 112,
}
}
5. 字段初始化简写
这是一个小tip,当结构体字段名与字段值对应变量名相同时,Rust支持使用字段初始化简写的方式:
fn build_user(email:String, username:String) -> User{
User{
emali,
username,
active: true,
sign_in_count: 112,
}
}
6. struct更新语法
在实际场景中,有一种情况很常见:根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 user2。
当你想要基于某个现有的struct实例来创建一个新实例的时候,在这种情况下,新实例中某些字段的值可能和现有的实例是相同的,而其他的字段和现有的实例可能存在不一样的情况。
我们想要实现这个效果,会这么做:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername456"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
这种复写的方式在实际编程过程中显得非常繁琐,对于这种情况,Rust给我们提供了结构体更新语法:
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername456"),
..user1
};
因为 user2 仅仅在 email 和 username 上与 user1 不同,因此我们只需要对这两者进行赋值,剩下的通过结构体更新语法 ..user1
即可完成。
..
语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1
必须在结构体的尾部使用。
7.struct 更新语法导致的所有权转移问题
通过一个例子进行观察:
fn main() {
#[derive(Debug)] //一个宏,支持通过 println!("{:?}", xx) 的方式打印结构体
struct User {
email: String,
username: String,
active: bool,
sign_in_count: u64,
}
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
println!("{}", user1.username); //报错:所有权已转移
println!("{}", user2.username); //someusername123
println!("{}", user1.email); //someone@example.com
println!("{}", user2.email); //another@example.com
println!("{}", user1.active); //true
println!("{}", user2.active); //true
println!("{}", user1.sign_in_count); //1
println!("{}", user2.sign_in_count); //1
println!("{:?}",user1); //打印整个结构体实例user1,报错:部分所有权已转移
println!("{:?}",user2); //打印整个结构体实例user2,正常输出
}
示例中的结构体在 user2 中创建了一个新实例,但该实例中 email 字段的值与 user1 不同,而 username、 active 和 sign_in_count 字段的值与 user1 相同。所以将..user1
放在了最后,指定除了 email 其余的字段都是从 user1 的相应字段中获取其值。
可是,当两个结构体实例创建完成后,在我们对他们进行打印输出的时候就遇到了问题。
结构体更新语法跟赋值语句 = 的原理非常相像,因此在上面代码中,user1 的部分字段所有权被转移到了 user2 中:username 字段发生了所有权转移,作为结果,user1 在一些时候无法再被使用。
聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 username 发生了所有权转移?
仔细回想一下
所有权
那一节的内容,我们提到了 Copy 特征:实现了 Copy 特征的类型无需所有权转移
,可以直接在赋值时
进行数据拷贝
,其中 bool 和 u64 类型就实现了 Copy 特征,因此 active 和 sign_in_count 字段在赋值给 user2 时,仅仅发生了拷贝
,而不是所有权转移。
知识点指路:所有权
Rust有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,则其一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。
那么哪些类型实现了 Copy 呢?你可以查看给定类型的文档来确认,不过作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy,任何不需要分配内存或某种形式资源的类型都可以实现 Copy 。
如下是一些 Copy 的类型:
- 所有整数类型,比如 u32。
- 布尔类型,bool ,它的值是 true 或者 false 。
- 所有浮点数类型,比如 f64。
- 字符类型,char。
- 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有,因为String没有实现Copy。
二、元组结构体
我们还可以定义类似 tuple (元组) 的 struct,叫做 tuple struct。tuple struct 整体有个名字,但里面的元素(字段)没有名字。
即:元组结构体整体有名称,但是元组结构体的字段没有名称。
例如:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
//black 和 origin 是不同的类型,是不同 tuple struct 的实例。
元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 Point
元组结构体,众所周知 3D 点是 (x, y, z)
形式的坐标点,因此我们无需再为内部的字段逐一命名为:x
, y
, z
。
三、单元结构体
在Rust中还可以定义没有任何字段
的 struct,叫做 Unit-Like struct,称为:单元结构体。
如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体
:
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
四、结构体数据的所有权
在之前的 User
结构体的定义中,有一处细节:我们使用了自身拥有所有权的 String
类型而不是基于引用的 &str
字符串切片类型。这是一个有意而为之的选择:因为我们想要这个结构体拥有它所有的数据,而不是从其它地方借用数据。
struct User{
username:String,
email:String,
sign_in_count:u64,
active:bool,
}
//这里的字段使用了 String 而不是 &str,表示:
//1.该struct实例拥有其所有的数据
//2.只要struct实例是有效的,那么里面的字段数据也是有效的
你也可以让 User
结构体从其它对象借用数据,不过这么做,就需要引入 生命周期(lifetimes) 这个新概念(一个复杂的概念),简而言之,生命周期能确保结构体的作用范围要比它所借用的数据的作用范围要小。
总之,如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错:
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
编译器会抱怨它需要生命周期标识符:
error[E0106]: missing lifetime specifier
--> src/main.rs:2:15
|
2 | username: &str,
| ^ expected named lifetime parameter // 需要一个生命周期
|
help: consider introducing a named lifetime parameter // 考虑像下面的代码这样引入一个生命周期
|
1 ~ struct User<'a> {
2 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:3:12
|
3 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | username: &str,
3 ~ email: &'a str,
|
未来在生命周期中会讲到如何修复这个问题以便在结构体中存储引用,不过在那之前,我们会避免在结构体中使用引用类型。
五、结构体的方法
方法 与函数在写法上类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。
不过方法与函数本质上是不同的,因为方法是在结构体的上下文中(impl中)被定义,并且它们第一个参数一般是 &self
,它代表调用该方法的结构体实例。
注意:方法的第一个参数可以是 &self ,也可以是获得其所有权的 self,或者是可变借用 &mut self。和函数的参数是类似的。通过编写方法,可以使我们的代码保持很好的组织性和可读性。
1. 定义方法
我们定义一个结构体 Rectangle
,这个结构体表示的是一个矩形,它有着 width 和 height 两个元素(字段)。
我们现在要给这个结构体专门定义一个“方法”,用以计算矩形的面积。当我们为一个给定的结构体定义方法时,需要添加impl
关键字,也就是在impl块
中编写结构体的一些方法。另外,一个结构体可以有多个impl块
存在。
在下面的代码中,我们给 Rectangle
结构体上成功定义了 area
方法,用来计算矩形的面积。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
//这里我们写&self,是因为只是借用一下实例的值,而不要它的所有权。
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
//结果:The area of the rectangle is 1500 square pixels.
注意:struct.xx() 这种形式通常被认为是调用结构体的某一个方法,而struct.xx 这种形式则是访问结构体的某一个字段的值。
还有一点可能你已经发现了,下面这部分代码有一点点可能不好理解的地方:
impl Rectangle {
fn area(&self) -> u32 {
//这里我们写&self,但是下面用的是self
self.width * self.height
}
}
这是因为,Rust 有一个叫 自动引用和解引用的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。
他是这样工作的:当使用 object.something()
调用方法时,Rust 会自动为 object
添加 &
、&mut
或 *
以便使 object
与方法签名匹配。也就是说,这些代码是等价的:
p1.distance(&p2);
(&p1).distance(&p2);
第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self
的类型。
在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self
),做出修改(&mut self
)或者是获取所有权(self
)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
2. 有多个参数的方法
方法和函数一样,可以使用多个参数:
#[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 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
//Can rect1 hold rect2? true
//Can rect1 hold rect3? false
3. 关联函数
现在大家可以思考一个问题,如何为一个结构体定义一个构造器方法?也就是接受几个参数,然后构造并返回该结构体的实例。其实答案在开头的代码片段中就给出了,很简单,参数中不包含 self
即可。
这种定义在 impl
中且没有 self
的函数被称之为关联函数: 因为它没有 self
,不能用 struct.xx()
的形式调用,因此它是一个函数而不是方法,它又在 impl
中,与结构体紧密关联,因此称为关联函数。
在之前的代码中,我们已经多次使用过关联函数,例如 String::from
,用于创建一个动态字符串。
impl Rectangle {
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
}
Rust 中有一个约定俗成的规则,使用
new
来作为构造器的名称,出于设计上的考虑,Rust 特地没有用new
作为关键字。
因为是函数,所以不能用 .
的方式来调用,我们需要用 ::
来调用,例如 let sq = Rectangle::new(3, 3);
。
其实,本质上是因为这个函数位于结构体的命名空间中,而 ::
语法就是用于关联函数和模块创建的命名空间。