使用结构的示例程序
为了了解我们何时可能想要使用结构体,让我们编写一个计算矩形面积的程序。我们将从使用单个变量开始,然后重构程序,直到我们改用结构体。
让我们使用 Cargo 制作一个名为 rectangles 的新二进制项目,该项目将采用以像素为单位指定的矩形的宽度和高度,并计算矩形的面积。示例 5-8 展示了一个简短的程序,其中一种方法可以在我们项目的 src/main.rs 中做到这一点。
文件名: src/main.rs
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 }
示例 5-8:计算由单独的 width 和 height 变量指定的矩形面积
现在,使用 cargo run
运行这个程序:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
此代码通过调用
area
函数,但我们可以做更多的事情来使这段代码清晰易读。
此代码的问题在 area
的签名中很明显:
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
函数应该计算一个矩形的面积,但我们编写的函数有两个参数,并且在我们的程序中的任何位置都不清楚这些参数是否相关。将 width 和 height 组合在一起会更具可读性和更易于管理。我们已经在第 3 章的 “元组类型” 一节中讨论了一种方法可以做到这一点:使用 Tuples。
使用 Tuples 进行重构
示例 5-9 显示了我们程序的另一个使用元组的版本。
文件名: src/main.rs
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 }
示例 5-9:使用元组指定矩形的宽度和高度
在某种程度上,这个程序更好。Tuples 让我们添加一些结构,我们现在只传递一个参数。但从另一个方面来说,这个版本就不那么清楚了:元组不命名它们的元素,所以我们必须对元组的各个部分进行索引,使我们的计算不那么明显。
混合宽度和高度对于面积计算无关紧要,但如果我们想在屏幕上绘制矩形,那就很重要了!我们必须记住,width
是元组索引 0,height
是元组索引
1
。如果其他人使用我们的代码,这将更难弄清楚并记住。因为我们没有在代码中传达数据的含义,所以现在更容易引入错误。
使用结构体进行重构:添加更多含义
我们使用结构体通过标记数据来添加含义。我们可以将正在使用的元组转换为一个结构体,该结构体具有整体名称,部分也具有名称,如示例 5-10 所示。
文件名: src/main.rs
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 }
示例 5-10:定义
Rectangle
结构体
在这里,我们定义了一个结构并将其命名为 Rectangle
。在大括号内,我们将字段定义为 width
和 height
,这两个字段的类型都是 u32
。然后,在 main
中,我们创建了一个特定的 Rectangle
实例
它的 width 为 30,height
为 50
。
我们的 area
函数现在使用一个参数定义,我们将其命名为
rectangle
,其类型是结构体 Rectangle
的不可变借用
实例。如第 4 章所述,我们想要借用结构体,而不是
拥有它的所有权。这样,main
保留了其所有权,并可以继续使用rect1
,这就是我们在函数签名中使用&
的原因,也是我们调用函数的地方。
area
函数访问 Rectangle
的 width
和 height
字段
实例(请注意,访问借用的结构实例的字段不会
移动字段值,这就是您经常看到 Borrows of Structs) 的原因。我们
area
的函数签名现在准确地说明了我们的意思:使用 Rectangle 的 width
和 height
字段计算 Rectangle
的面积。这传达了 width 和 height 彼此相关,并且它为值提供了描述性名称,而不是使用 0
和 1
的 Tuples 索引值。这是一场清晰的胜利。
使用派生特征添加有用的功能
如果我们在调试程序时能够打印 Rectangle
的实例并查看其所有字段的值,那将非常有用。示例 5-11 尝试使用我们在前几章中使用的 println!
宏。但是,这行不通。
文件名: src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
示例 5-11:尝试打印
Rectangle
实例
当我们编译这段代码时,我们会收到一个错误,其中包含以下核心消息:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
宏可以执行多种格式设置,默认情况下,大括号指示 println!
使用称为 Display
: 输出的格式,供最终用户直接使用。到目前为止,我们所看到的基元类型默认实现 Display
,因为只有一种方式可以向用户显示 1
或任何其他基元类型。但是对于结构体,方式
println!
应该格式化输出不太清楚,因为有更多的显示可能性:你想要逗号还是不要?是否要打印大括号?是否应显示所有字段?由于这种歧义,Rust 不会尝试猜测我们想要什么,并且结构体没有提供的 Display
实现来与 println!
和 {}
占位符一起使用。
如果我们继续阅读这些错误,我们会发现这个有用的说明:
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
让我们试试吧!println!
宏调用现在将类似于 println!("rect1 is {rect1:?}");
。将说明符 :?
放在大括号内表示
println!
我们想使用一种名为 Debug
的输出格式。Debug
trait 使我们能够以对开发人员有用的方式打印我们的结构体,这样我们就可以在调试代码时看到它的值。
使用此更改编译代码。妈的!我们仍然收到一个错误:
error[E0277]: `Rectangle` doesn't implement `Debug`
但同样,编译器为我们提供了一个有用的说明:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Rust 确实包含打印调试信息的功能,但我们必须明确选择使该功能可用于我们的结构体。为此,我们在结构体定义之前添加外部属性 #[derive(Debug)]
,如示例 5-12 所示。
文件名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
示例 5-12:添加属性以派生
Debug
trait 并使用调试格式打印 Rectangle
实例
现在,当我们运行该程序时,我们不会收到任何错误,我们将看到以下输出:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
好!这不是最漂亮的输出,但它显示了此实例的所有字段的值,这肯定会在调试过程中有所帮助。当我们有更大的结构体时,拥有更易于阅读的输出是很有用的;在这些情况下,我们可以在 println!
字符串中使用 {:#?}
而不是 {:?}
。在此示例中,使用 {:#?}
样式将输出以下内容:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
使用 Debug
格式打印值的另一种方法是使用 dbg!
macro 的所有权(与采用引用的 println!
相反),打印代码中发生 dbg!
macro 调用的文件和行号以及该表达式的结果值,并返回该值的所有权。
注意:调用 dbg!
宏将打印到标准错误控制台流 (stderr
),而 println!
将打印到标准输出控制台流 (stdout
)。我们将在
将错误消息写入标准错误而不是标准输出
第 12 章中的部分。
下面是一个示例,我们对分配给
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); }
我们可以将 dbg!
放在表达式 30 * scale
周围,因为 dbg!
返回表达式值的所有权,width
字段将获得与我们没有 dbg!
调用相同的值。我们不希望 dbg!
获得 rect1
的所有权,因此我们在下一次调用中使用对 rect1
的引用。此示例的输出如下所示:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
我们可以看到输出的第一位来自 src/main.rs 第 10 行,我们正在调试表达式 30 * scale
,其结果值为 60
(
为整数实现的调试
格式是仅打印其值)。这
dbg!
在 src/main.rs 的第 14 行调用 output &rect1
的值,即 Rectangle
结构。此输出使用
Rectangle
类型。dbg!
宏在你试图弄清楚你的代码在做什么时真的非常有用!
除了 Debug
trait 之外,Rust 还为我们提供了许多 trait 供我们使用 derive
属性,这些 trait 可以为我们的自定义类型添加有用的行为。这些特征及其行为在附录 C 中列出。在第 10 章中,我们将介绍如何使用自定义行为实施这些特征,以及如何创建自己的特征。除了 derive
之外,还有许多属性;有关详细信息,请参阅“属性”
部分。
我们的 area
函数非常具体:它只计算矩形的面积。将此行为更紧密地绑定到我们的 Rectangle
结构体会很有帮助,因为它不适用于任何其他类型的结构。让我们看看如何通过将 area
函数转换为 area
方法来继续重构此代码
定义在我们的 Rectangle
类型上。