The Rust programming language 读书笔记——结构体(Struct)

结构(Struct)是一种自定义数据类型。允许我们命名多个相关的值并将它们组成一个有机的结合体。

定义与实例化

关键字 struct 被用来定义并命名结构体,一个良好的结构体名称需反映出自身数据组合的意义。

1
2
3
4
5
6
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

结构体就像是类型的通用模板,将具体的数据填入模板时就创建了新的实例

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

在创建了结构体实例后,可以通过点号来访问实例中的特定字段。假如这个实例是可变的,还可以通过点号来修改字段的值。

1
2
3
4
5
6
7
8
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");

需要注意的是,一旦结构体实例定义为可变,那么实例中的所有字段都将是可变的

可以在函数体的最后一个表达式中构建结构体实例,来隐式的将这个实例作为结果返回。

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}

在变量名与字段名相同时,可以使用简化版的字段初始化方法重构上面的 build_user 函数。

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

在许多情况下,新创建的实例中,除了需要修改的小部分字段以外,其余字段的值与旧实例完全相同。可以使用结构体更新语法快速实现此类新实例的创建。

使用结构体更新语法来为一个 User 实例设置新的 email 和 username 字段的值,并从 user1 实例中获取剩余字段的值:

1
2
3
4
5
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};

.. 表示剩下的那些还未被显式赋值的字段都与给定实例拥有相同的值。

元组结构体

可以使用一种类似元组的方式定义结构体,这种结构体也被称作元组结构体。元组结构体同样拥有表明自身含义的名称,但无需在声明时对其字段进行命名,只标注类型即可。

1
2
3
4
5
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

这里的 black 和 origin 是不同的类型,因为它们两个分别是不同元组结构体的实例。
每一个结构体都拥有自己的类型

示例程序

使用 cargo 命令创建一个名为 rectangles 的项目:
cargo new rectangles
这个程序会接收以像素为单位的宽度和高度作为输入,并计算出对应的长方形面积。

编辑项目中的 src/main.rs 源代码文件:

1
2
3
4
5
6
7
8
9
10
fn main() {
let width1 = 30;
let height1 = 50;

print!("The area of the rectangle is {}", area(width1, height1));
}

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

运行 cargo run 命令查看输出:

1
2
3
4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/rectangle`
The area of the rectangle is 1500

area 函数用来计算长方形的面积,接收宽和高两个参数。这两个参数是相互关联的,但程序中没有任何地方可以体现这一点。将宽和高放在一起能够使代码更加易懂和易于维护。

使用元组关联长方形的宽和高

1
2
3
4
5
6
7
8
fn main() {
let rect1 = (30, 50);
print!("The area of the rectangle is {}", area(rect1));
}

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

在上面的代码中,元组使输入的参数结构化了,现在只需要传递一个参数就可以调用函数 area
但元组不会给出自身元素的名称,只能通过索引访问。这使得程序变得难以阅读。
比如当需要将该长方形绘制到屏幕上时,混淆宽度和高度就容易出现问题。

使用结构体增加有意义的描述信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
print!("The area of the rectangle is {}", area(&rect1));
}

fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}

Rectangle 结构体表明了宽度和高度是相互关联的两个值,并为这些值提供了描述性的名字。因此代码看起来会更加清晰。

方法

方法与函数十分相似,它们都使用 fn 关键字及一个名称进行声明;它们都可以拥有参数和返回值;它们都包含了一段在调用时执行的代码。
方法总是被定义在某个结构体(或者枚举类型、trait 对象)的上下文中,且它们的第一个参数都是 self,用于指代调用该方法的结构体实例

area 函数定义为 Rectangle 结构体中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,
};
print!("The area of the rectangle is {}", rect1.area());
}

由于方法的声明被放置在 impl Rectangle 块中,因此 Rust 能够将 self 的类型推导为 Rectangle,我们才可以在 area 的签名中使用 &self 来替代 &Rectangle
使用方法替代函数不仅能够避免在每个方法的签名中重复编写 self 的类型,还有助于程序员组织代码的结构。可以将某个类型的实例需要的功能放置在同一个 impl 块中,避免用户在代码库中盲目地搜索它们。

添加 can_hold 方法检测当前的 Rectangle 实例能否完整地包含传入的另一个 Rectangle 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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,
};

print!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
print!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

关联函数

除了方法,impl 块还允许我们定义不用接收 self 作为参数的函数。这类函数与结构体(而不是实例)相互关联,因此也被称为关联函数。
它们不会作用于某个具体的结构体实例。
之前用到的 String::from 就是关联函数的一种。

关联函数常被用作构造器来返回一个结构体的新实例。例如可以编写一个 square 关联函数,只接收一个参数,该参数同时用作宽度与高度来构造正方形实例。

1
2
3
4
5
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}

这样就可以使用 let sq = Rectangle::square(3); 类似的语法来创建正方形实例。

总结

结构体可以让我们基于特定领域的规则创建有意义的自定义类型
通过使用结构体,可以将相互关联的数据组合起来,并为每条数据赋予有含义的名称,从而使代码更加清晰。
方法可以让我们为结构体实例指定特殊的行为,而关联函数则可以将那些不需要实例的特定功能放置到结构体的命名空间中。

参考资料

The Rust Programming Language