Rust-结构体

一、定义和实例化结构体

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

定义结构体,需要使用 struct 关键字并为整个结构体提供一个名字。结构体的名字需要描 述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,它们被称作 字段 (field),并定义字段类型。

struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。 创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value 对的形式提供字段,其中 key 是字段的名字,value 是需要储存在字段中的数据值。

我们可以声明一个特定的用户:

let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

要从结构中获取特定值,我们可以使用点表示法。如果我们只想要这个用户的电子邮件地址,我们可以user1.email在任何想要使用这个值的地方使用。如果实例是可变的,我们可以通过使用点表示法并分配到特定字段来更改值。

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");

注意整个实例必须是可变的;Rust 不允许我们只将某些字段标记为可变的。与任何表达式一样,我们可以构造一个新的结构实例作为函数体中的最后一个表达式,以隐式返回该新实例。
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}

这是有道理的命名具有相同名称与该结构域的功能参数,而不必重复email和username字段名和变量是有点乏味。如果结构体有更多字段,重复每个名称会变得更加烦人。幸运的是,有一个方便的速记!

变量与字段同名时的字段初始化简写语法

上面代码中参数名与字段名都完全相同,我们可以使用 字段初始化简写语法(field init shorthand)来重写 build_user ,这样其行为与之前完全相同,不过无需重复 email 和 username 了

fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

在这里,我们创建了User结构体的一个新实例,它有一个名为 的字段email。我们想将email字段的值 设置email为build_user函数参数中的值。因为email字段和email参数同名,所以我们只需要写email而不是email: email.

使用结构体更新语法从其他对象创建对象

创建一个结构的新实例通常很有用,该实例使用大部分旧实例的值但会更改一些值。您可以使用struct update 语法来做到这一点。
User在user2没有更新语法的情况下创建一个新实例。我们为 设置了一个新值,email但在其他方面使用了user1:

let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};

使用struct update语法,我们可以用更少的代码实现同样的效果,语法..指定未显式设置的其余字段应与给定实例中的字段具有相同的值。
let user2 = User {
email: String::from("another@example.com"),
..user1
};

struct update 语法类似于赋值,移动数据。在这个例子中,我们user1 在创建后不能再使用了,user2因为String的username字段user1 被移到了user2.

使用没有命名字段的元组结构体来创建不同的类型

也可以定义与元组类似的结构体,称为 元组结构体(tuple structs),有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。元组结构体在你希望命名整个元组并使其与其他(同样的)元组为不同类型时很有用,这时像常规结构体那样为每个字段命名就显得冗余和形式化了。
定义元组结构体以 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 值是不同的类型,因为它们是不同的元组结构体的实例。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 . 后跟 索引来访问单独的值,等等。

没有任何字段的类单元结构体

我们也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体(unit-like structs) 因为它们类似于 () ,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型内存储数据的时候发挥作用。
下面是一个声明和实例化名为 的单元结构的例子AlwaysEqual:

fn main() {
struct AlwaysEqual;

let subject = AlwaysEqual;
}

要定义AlwaysEqual,我们使用struct关键字,我们想要的名称,然后是分号。不需要大括号或圆括号!然后,我们可以得到的一个实例,AlwaysEqual在subject以类似的方式变量:使用我们定义的名称,没有任何花括号或括号。

结构体数据的所有权

在前面代码中,我们使用了拥有的String 类型而不是&str字符串切片类型。这是一个深思熟虑的选择,因为我们希望该结构的实例拥有其所有数据,并且只要整个结构有效,该数据就有效。
结构体可以存储对其他事物拥有的数据的引用,但这样做需要使用生命周期,后续将会介绍。生命周期确保结构引用的数据在同样长的时间内有效就像结构一样。假设您尝试在不指定生命周期的情况下将引用存储在结构中,如下所示,这是行不通的:

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,
};
}

编译器会抱怨它需要生命周期说明符:
cargo run
Compiling structs v0.1.0 (file:///projects/structs)
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,
|

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs`

To learn more, run the command again with --verbose.

二、一个使用结构体的示例程序

要了解何时可能需要使用结构体,让我们编写一个计算矩形面积的程序。我们将从单个变量开始,然后重构程序直到我们使用结构体。
让我们用 Cargo 创建一个名为矩形的新二进制项目,它将获取以像素为单位指定的矩形的宽度和高度并计算矩形的面积。

fn main() {
let width1 = 30;
let height1 = 50;

println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}

fn area(width: u32, height: u32) -> u32 {
width * height
}

此代码的问题在以下签名中很明显area:
fn area(width: u32, height: u32) -> u32 {

该area函数应该计算一个矩形的面积,但我们编写的函数有两个参数。参数是相关的,但在我们的程序中没有任何地方表达。将宽度和高度组合在一起会更易读且更易于管理。

用元组重构

基于上面的代码,我们给出了元组的版本:

fn main() {
let rect1 = (30, 50);

println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}

fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}

在某种程度上说这个程序更好一点了。元组帮助我们增加了一些结构性,现在在调用 area 的时候只需传递一个参数。不过在另一方面这个方法却更不明确了:元组并没有给出它元素 的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:
在面积计算时混淆宽高并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将 不得不记住元组索引 0 是 length 而 1 是 width 。如果其他人要使用这些代码,他们也 不得不搞清楚并记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中 数据的意义。

使用结构体重构:赋予更多意义

我们使用结构体为数据命令来为其赋予意义。我们可以将元组转换为一个有整体名称而且每
个部分也有对应名字的数据类型.

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
}

这里我们定义了一个结构体并称其为 Rectangle 。在 {} 中定义了字段 length 和width ,都是 u32 类型的。接着在 main 中,我们创建了一个宽度为 30 和高度为 50 的 Rectangle 的具体实例。

函数 area 现在被定义为接收一个名叫 rectangle 的参数,其类型是一个结构体Rectangle 实例的不可变借用。我们希望借用结构体而不是获取它的所有权,这样 main 函数就可以保持 rect1 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &

通过派生 trait 增加实用功能

如果能够在调试程序时打印出 Rectangle 实例来查看其所有字段的值就更好了。

struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect1 is {}", rect1);
}

当我们编译这段代码时,我们会收到一个带有这个核心消息的错误:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

println! 宏能处理很多类型的格式,不过, {} 默认告诉 println! 使用被称为 Display 的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 Display ,因为它就是向用户展示 1 或其他任何基本类型的唯一方式。
不过对于结构体, println! 应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?由于这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个 Display 实现。
{} 中加入 :? 指示符告诉 println! 我们想要使用叫做 Debug 的输出格式Debug 是 一个 trait,它允许我们在调试代码时以一种对开发者有帮助的方式打印出结构体。
println!("rect1 is {:?}", rect1);

使用此更改编译代码。天啊!我们仍然得到一个错误:
error[E0277]: `Rectangle` doesn't implement `Debug`

Rust 确实 包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此, 在结构体定义之前加上 #[derive(Debug)] 注解:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect1 is {:?}", rect1);
}

现在我们再运行这个程序时,就不会有任何错误并会出现如下输出了:
rect1 is Rectangle { width: 30, height: 50 }

当我们有一个更大的结构体时,能有更易读一点的输出就好了,为此可以使用 {:#?} 替 换 println! 字符串中的 {:?} 。如果在这个例子中使用了 {:#?} 风格的话,输出会看起来 像这样:
rect1 is Rectangle {
width: 30,
height: 50
}

使用Debug格式打印出值的另一种方法是使用 dbg!宏 。该dbg!宏获取表达式的所有权,打印dbg!代码中该宏调用发生位置的文件和行号以及该表达式的结果值,并返回该值的所有权。调用dbg!宏会打印到标准错误控制台流 (stderr),而不是println!打印到标准输出控制台流 (stdout)

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};

dbg!(&rect1);
}

Rust 还提供了许多 trait 供我们与derive属性一起使用,这些属性可以为我们的自定义类型添加有用的行为。

三、方法语法

方法与函数类似: 它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含一段该方法在某处被调用时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义,并且它们第一个参数总是 &self ,它代表调用该方法的结构体实例。

定义方法

让我们更改area具有Rectangle实例作为参数的函数,而是area在Rectangle结构上定义一个方法,如下:

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}

为了使函数定义于 Rectangle 的上下文中,开始了一个 impl 块。然后在 main 中将我们先前调用 area 方法并传递 rect1 作为参数的地方,改成使用方法语法(method syntax) 在 Rectangle 实例上调用 area 方法。方法语法获取一个实例并加上一个点号,后跟方法名、括号以及任何参数。

在 area 的签名中,开始使用 &self 来替代 rectangle: &Rectangle ,因为该方法位于impl Rectangle 上下文中所以 Rust 知道 self 的类型是 Rectangle 。注意仍然需要在self 前面加上 & ,就像 &Rectangle 一样。方法可以选择获取 self 的所有权,或者像我们这里一样不可变地借用 self ,或者可变地借用 self ,就跟其他别的参数一样。

这里选择 &self 跟在函数版本中使用 &Rectangle 出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self 。通过仅仅使用 self 作为第一个参数来使方法获取 实例的所有权是很少见的;这种技术通常用在当方法将 self 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。

使用方法替代函数,除了使用了方法语法和不需要在每个函数签名中重复 self 类型之外, 其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 impl 块中,而不是让将来的用户在我们的库中到处寻找 Rectangle 的功能。

-> 运算符到哪去了?

像在 C++ 这样的语言中,有两个不同的运算符来调用方法: . 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果 object 是一个指针,那么 object->something() 就像(*object).something() 一样。

Rust 并没有一个与 -> 等效的运算符; 相反,Rust 有一个叫 自动引用和解引用 (automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

这是它如何工作的:当使用 object.something() 调用方法时,Rust 会自动增加&&mut* 以便使 object 符合方法的签名。也就是说,这些代码是等价的:

p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者———— self 类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取&self,做出修改&mut self或者是获取所有权self。 Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。

带有更多参数的方法

让我们更多的实践一下方法,通过为 Rectangle 结构体实现第二个方法。这回,我们让一个 Rectangle 的实例获取另一个 Rectangle 实例并返回 self 能否完全包含第二个长方形,如果能则返回 true ,如果不能则返回 false 。

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));
}

同时我们希望看到如下输出,因为 rect2 的宽高都小于 rect1 ,而 rect3 比 rect1 要宽:
Can rect1 hold rect2? true
Can rect1 hold rect3? false

因为我们想定义一个方法,所以它应该位于 impl Rectangle 块中。方法名是 can_hold ,并 且它会获取另一个 Rectangle 的不可变借用作为参数。通过观察调用位置的代码可以看出参 数是什么类型的: rect1.can_hold(&rect2) 传入了 &rect2 ,它是一个 Rectangle 的实例rect2 的不可变借用。这是可以理解的,因为我们只需要读取 rect2 (而不是写入,这意味着我们需要一个可变借用)而且希望 main 保持 rect2 的所有权这样就可以在调用这个方法后继续使用它。 can_hold 的返回值是一个布尔值,其实现会分别检查 self 的宽高是否都大于另一个 Rectangle 。
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 函数。
关联函数经常被用作返回一个结构体新实例的构造函数。

impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}

要调用此关联函数,我们使用::带有结构名称的语法; let sq = Rectangle::square(3);是一个例子。此函数由结构命名空间:该::语法用于关联函数和模块创建的命名空间。

多个 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
}
}