Rust学习笔记3 - 结构体

本文是 Rust 学习笔记系列第三篇,参考 Rust 指南的第 5 章,涉及结构体、方法。

结构体(struct)是一种自定义的数据类型,用于将多个相关值组织成一个有意义的组合。如果熟悉面向对象的语言,会发现它和对象的数据属性类似。

接下来我们会比较结构体与元组的差异,演示如何使用结构体,讨论如何定义方法与关联函数。

结构体的定义与实例化

结构体和元组类似,可以由不同类型的数据组成,但是需要对每个部分的数据进行命名,这些命名的数据就被称为字段(fields)。

Rust 中,使用 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 u1 = User {
email: String::from("someone@example.com"),
username: String::from("someone123"),
active: true,
sign_in_count: 1,
}

我们可以用点语法来访问结构体的字段(instance.field_name),并且如果实例是可变的,我们就可以修改字段的内容。

Rust 不允许结构体的部分字段可变,只能是结构体整体可变或者不可变。

实例化语法也是一种表达式,因此我们可以用如下写法的函数创建实例:

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

结构体字段省略写法

当结构体的字段名与数据的变量名一致时,可以省略变量名的部分:

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

结构体更新语法

从旧的结构体修改部分属性来创建新的结构体是很常见的写法,我们可以用结构体更新语法来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
let u2 = User {
email: String::from("another@example.com"),
username: String::from("another567"),
active: u1.active,
sign_in_count: u1.sign_in_count,
}

// 结构体更新语法
let u2 = User {
email: String::from("another@example.com"),
username: String::from("another567"),
..u1
}

使用元组结构体来创建不同的类型

有一种结构体与元组类似,不为字段命名。元组结构体通常用于给元组类型命名,并且要与其他元组作区分时使用。

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

单位结构体

有一种结构体与单位类型(())类似,不包含任何字段。单位结构体在当需要对某种类型实现特性但不需要任何字段时很有用。

结构体数据的所有权

在上面的结构体定义中,我们没有使用引用,因为我们想要结构体拥有字段的所有权,这样字段的有效期与结构体一致了。

但是在结构体中是可以使用引用类型的字段的,但是需要我们使用 Rust 的生命周期(lifetimes)功能,生命周期用于保证引用的数据在结构体使用过程中有效。

一个使用结构体的例子

为了理解结构体的例子,我们写一个计算矩形面积的程序。

先从简单的变量开始写:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 用于计算长方形的面积,但是却传入了两个整数类型的参数,表意并不明确。将这两个整数组合起来更具有可读性和可维护性。

使用元组来重构之:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let rect1 = (30, 50);

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

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

在面积计算中弄混宽和高并没有什么问题,可是如果我们需要绘制长方形,就不能弄错了。我们可以用结构体来使字段含义更加明确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Rect {
w: u32,
h: u32,
}

fn main() {
let rect1 = Rect { w: 30, h: 50 };

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

fn area(rect: &Rect) -> u32 {
rect.w * rect.h
}

使用衍生特性来添加功能

在调试程序时,如果能够打印出结构体的内容会很方便。

如果我们尝试打印结构体,我们会得到编译错误:

1
2
let rect1 = Rect { w: 30, h: 50 };
println!("rect1 is {}", rect1);
1
2
3
4
5
error[E0277]: `Rect` doesn't implement `std::fmt::Display`
= help: the trait `std::fmt::Display` is not implemented for `Rect`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required by `std::fmt::Display::fmt`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

println! 宏可以做很多格式化的事情,默认情况下,{} 用于提示 println! 使用 Display 来格式化(输出终端用户普遍预期的内容),前面介绍的基本类型都已经实现了 Display,因为基本类型的预期输出是固定的。但是结构体应该输出什么内容是不确定的,因此结构体不会提供 Display

接着阅读错误提示,我们可以看到有提示让我们换成 {:?} 来格式化,我们改过来试试:

1
2
let rect1 = Rect { w: 30, h: 50 };
println!("rect1 is {:?}", rect1);

得到的仍然是编译错误:

1
2
3
4
5
error[E0277]: `Rect` doesn't implement `std::fmt::Debug`
= help: the trait `std::fmt::Debug` is not implemented for `Rect`
= note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
= note: required by `std::fmt::Debug::fmt`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

我们可以看到有提示让我们使用 #[derive(Debug)]

Rust 有提供输出调试信息的功能,但是需要显式地将该功能应用于我们的结构体,我们需要在结构体定义的前面加上 #[derive(Debug)] 标记。

1
2
3
4
5
6
7
8
9
10
#[derive(Debug)]
struct Rect {
w: u32,
h: u32,
}

fn main() {
let rect1 = Rect { w: 30, h: 50 };
println!("rect1 is {:?}", rect1);
}

可以打印出如下内容:

1
rect1 is Rect { w: 30, h: 50 }

如果使用 {:#?} 则会打印:

1
2
3
4
rect1 is Rect {
w: 30,
h: 50,
}

方法

上面的 area 的含义已经很明显了:计算矩形的面积。但是我们需要将这个方法与 Rect 结构体结合地更加紧密,因为它不能用于其他的结构体。我们可以将它转换成方法。

方法(Methods)与函数类似:使用 fn 定义,拥有名称,参数,返回值,函数体。然而不同之处在于,方法是在结构体(或者枚举或特性对象)的上下文内定义的,而且第一个参数始终是 &self,引用调用方法的结构体的实例。

定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#[derive(Debug)]
struct Rect {
w: u32,
h: u32,
}

impl Rect {
fn area(&self) -> u32 {
self.w * self.h
}
}

fn main() {
let rect1 = Rect { w: 30, h: 50 };
println!("rect1 is {:?}", rect1);
println!("area of rect1 is {}", rect1.area());
}

要在函数体的上下文中定义函数,需要使用 impl 块。要调用方法,我们需要用方法调用的语法,例如:rect1.area()

在方法的定义中,我们使用 &self 代替了 rect: &Rect,因为通过上下文可以知道其类型。需要注意这里我们仍然需要使用引用。和函数类似,方法可以拿走调用者所有权,或者使用不可变引用,或者使用可变引用。这里我们使用不可变引用是因为我们不需要所有权,且不会修改数据。如果在方法中需要修改结构体数据,就需要可变引用(&mut self)。

在 C 和 C++ 中,有两个不同的操作符用于调用方法:instance.method()pointer->method()(等价于 (*pointer).method()

然而在 Rust 中,没有与 -> 等价的操作符。因为 Rust 有自动引用和解引用的特性。调用方法是少数几个用到这个特性的地方。
当调用方法时,Rust 会自动使用 & &mut * 来让调用者符合方法的签名

给方法增加参数

我们试着给 Rect 结构体增加新的方法,这个方法需要接受一个别的 Rect 实例,并且当调用方能够包含这另一个矩形时返回 true

1
2
3
4
5
6
7
8
9
impl Rect {
fn area(&self) -> u32 {
self.w * self.h
}

fn can_hold(&self, other: &Rect) -> bool {
self.w > other.w && self.h > other.h
}
}

关联函数

impl 块的另一个有用的功能是,我们可以在其中定义一些没有 self 参数的函数。这些被称为是「关联函数」(associated functions)。关联函数经常用于结构体的构造函数,比如我们可以给 Rect 增加一个正方形的构造函数:

1
2
3
4
5
6
7
8
9
10
impl Rect {
fn square(size: u32) -> Rect {
Rect { w: size, h: size }
}
}

fn main() {
let sq1 = Rect::square(3);
println!("area of sq1 is {}", sq1.area());
}

我们用 :: 操作符来调用结构体的关联函数(或者模块的命名空间)。

多个 impl

结构体允许使用多个 impl 块,这和都写在一个 impl 块里是等价的。

目前并不会用到多个 impl 块,但是之后说到范型和特性时,会再提到。

总结

结构体可以用于定义具有行业特定含义的自定义类型。使用结构体,可以将关联的代码组织在一起,并且与其他的部分不会产生命名冲突。

# Rust
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×