Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T13:45:01Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: bebb0dfbd5dd94bfcd19f9d2f6216fcac10a9b168eef6a07a1d753ae85512557 source_path: ch05-02-example-structs.md workflow: 16

一个使用结构体的示例程序 (An Example Program Using Structs)

An Example Program Using Structs

为了理解我们什么时候可能想要使用结构体,让我们编写一个计算长方形面积的程序。我们将从使用单个变量开始,然后重构程序,直到改为使用结构体。

To understand when we might want to use structs, let’s write a program that calculates the area of a rectangle. We’ll start by using single variables and then refactor the program until we’re using structs instead.

让我们用 Cargo 创建一个名为 rectangles 的新二进制项目,它将接收以像素为单位的长方形宽度和高度,并计算长方形的面积。示例 5-8 展示了一个简短的程序,在项目的 src/main.rs 中以一种方式实现了这一点。

Let’s make a new binary project with Cargo called rectangles that will take the width and height of a rectangle specified in pixels and calculate the area of the rectangle. Listing 5-8 shows a short program with one way of doing exactly that in our project’s src/main.rs.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/src/main.rs:all}}
}

现在,使用 cargo run 运行此程序:

Now, run this program using cargo run:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/output.txt}}

这段代码通过调用带有每个维度的 area 函数成功算出了长方形的面积,但我们可以做更多工作来使代码清晰易读。

This code succeeds in figuring out the area of the rectangle by calling the area function with each dimension, but we can do more to make this code clear and readable.

这段代码的问题在 area 的签名中很明显:

The issue with this code is evident in the signature of area:

{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-08/src/main.rs:here}}

area 函数本应计算“一个”长方形的面积,但我们编写的函数有两个参数,而且在程序的任何地方都没有明确表明这两个参数是相关的。将宽度和高度组合在一起会更易读且更易于管理。我们已经在第 3 章的 “元组类型” 部分讨论了实现这一点的一种方法:使用元组。

The area function is supposed to calculate the area of one rectangle, but the function we wrote has two parameters, and it’s not clear anywhere in our program that the parameters are related. It would be more readable and more manageable to group width and height together. We’ve already discussed one way we might do that in “The Tuple Type” section of Chapter 3: by using tuples.

使用元组重构 (Refactoring with Tuples)

Refactoring with Tuples

示例 5-9 展示了程序的另一个使用元组的版本。

Listing 5-9 shows another version of our program that uses tuples.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-09/src/main.rs}}
}

从某种角度来说,这个程序更好。元组让我们增加了一点结构,而且我们现在只传递一个参数。但从另一种角度来说,这个版本不太清晰:元组没有命名它们的元素,所以我们必须通过索引访问元组的部分,这使得我们的计算不够直观。

In one way, this program is better. Tuples let us add a bit of structure, and we’re now passing just one argument. But in another way, this version is less clear: Tuples don’t name their elements, so we have to index into the parts of the tuple, making our calculation less obvious.

混淆宽度和高度对于面积计算来说并不重要,但如果我们想在屏幕上画出这个长方形,那就重要了!我们将不得不记住 width 是元组索引 0,而 height 是元组索引 1。如果别人使用我们的代码,这对于他们来说会更难弄清楚并记住。因为我们没有在代码中传达数据的含义,现在更容易引入错误。

Mixing up the width and height wouldn’t matter for the area calculation, but if we want to draw the rectangle on the screen, it would matter! We would have to keep in mind that width is the tuple index 0 and height is the tuple index 1. This would be even harder for someone else to figure out and keep in mind if they were to use our code. Because we haven’t conveyed the meaning of our data in our code, it’s now easier to introduce errors.

使用结构体重构:赋予更多意义 (Refactoring with Structs)

Refactoring with Structs

我们使用结构体通过标记数据来赋予意义。我们可以将正在使用的元组转换为结构体,既为整体命名,也为部分命名,如示例 5-10 所示。

We use structs to add meaning by labeling the data. We can transform the tuple we’re using into a struct with a name for the whole as well as names for the parts, as shown in Listing 5-10.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-10/src/main.rs}}
}

在这里,我们定义了一个名为 Rectangle 的结构体。在花括号内,我们将字段定义为 widthheight,它们的类型都是 u32。然后,在 main 中,我们创建了 Rectangle 的一个特定实例,其宽度为 30,高度为 50

Here, we’ve defined a struct and named it Rectangle. Inside the curly brackets, we defined the fields as width and height, both of which have type u32. Then, in main, we created a particular instance of Rectangle that has a width of 30 and a height of 50.

我们的 area 函数现在定义了一个参数,我们将其命名为 rectangle ,其类型是结构体 Rectangle 实例的一个不可变借用。正如第 4 章所述,我们希望借用结构体而不是获取它的所有权。这样,main 就可以保留所有权并继续使用 rect1,这也是我们在函数签名和调用函数的地方使用 & 的原因。

Our area function is now defined with one parameter, which we’ve named rectangle, whose type is an immutable borrow of a struct Rectangle instance. As mentioned in Chapter 4, we want to borrow the struct rather than take ownership of it. This way, main retains its ownership and can continue using rect1, which is the reason we use the & in the function signature and where we call the function.

area 函数访问 Rectangle 实例的 widthheight 字段(注意访问借用的结构体实例的字段不会移动字段值,这就是为什么你经常看到结构体的借用)。我们的 area 函数签名现在准确表达了我们的意思:使用 Rectanglewidthheight 字段计算它的面积。这传达了宽度和高度是相互关联的,并且为这些值赋予了描述性的名称,而不是使用元组索引值 01。这在清晰度上是一种进步。

The area function accesses the width and height fields of the Rectangle instance (note that accessing fields of a borrowed struct instance does not move the field values, which is why you often see borrows of structs). Our function signature for area now says exactly what we mean: Calculate the area of Rectangle, using its width and height fields. This conveys that the width and height are related to each other, and it gives descriptive names to the values rather than using the tuple index values of 0 and 1. This is a win for clarity.

通过派生特征添加功能 (Adding Functionality with Derived Traits)

Adding Functionality with Derived Traits

能够在调试程序时打印 Rectangle 的实例并查看其所有字段的值将非常有用。示例 5-11 尝试使用我们在前几章中使用过的 println!。然而,这行不通。

It’d be useful to be able to print an instance of Rectangle while we’re debugging our program and see the values for all its fields. Listing 5-11 tries using the println! macro as we have used in previous chapters. This won’t work, however.

{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/src/main.rs}}

当我们编译这段代码时,会得到一个包含核心消息的错误:

When we compile this code, we get an error with this core message:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/output.txt:3}}

println! 宏可以执行多种格式化,默认情况下,花括号告诉 println! 使用称为 Display 的格式化:旨在直接供最终用户使用的输出。到目前为止我们见过的原始类型默认都实现了 Display,因为你只想向用户显示 1 或任何其他原始类型的一种方式。但对于结构体,println! 应该如何格式化输出就不那么明确了,因为有更多的显示可能性:你要逗号吗?你要打印花括号吗?所有字段都要显示吗?由于这种歧义,Rust 不会尝试猜测我们想要什么,而且结构体没有提供可以与 println!{} 占位符一起使用的 Display 实现。

The println! macro can do many kinds of formatting, and by default, the curly brackets tell println! to use formatting known as Display: output intended for direct end user consumption. The primitive types we’ve seen so far implement Display by default because there’s only one way you’d want to show a 1 or any other primitive type to a user. But with structs, the way println! should format the output is less clear because there are more display possibilities: Do you want commas or not? Do you want to print the curly brackets? Should all the fields be shown? Due to this ambiguity, Rust doesn’t try to guess what we want, and structs don’t have a provided implementation of Display to use with println! and the {} placeholder.

如果我们继续阅读错误,会发现这条有用的提示:

If we continue reading the errors, we’ll find this helpful note:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-11/output.txt:9:10}}

让我们试试看!println! 宏调用现在看起来像 println!("rect1 is {rect1:?}");。在花括号内放入限定符 :? 告诉 println! 我们想要使用一种名为 Debug 的输出格式。Debug 特征使我们能够以一种对开发者有用的方式打印结构体,以便我们在调试代码时查看其值。

Let’s try it! The println! macro call will now look like println!("rect1 is {rect1:?}");. Putting the specifier :? inside the curly brackets tells println! we want to use an output format called Debug. The Debug trait enables us to print our struct in a way that is useful for developers so that we can see its value while we’re debugging our code.

更改后编译代码。糟糕!还是报错:

Compile the code with this change. Drat! We still get an error:

{{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-01-debug/output.txt:3}}

但编译器再次给了我们一条有用的提示:

But again, the compiler gives us a helpful note:

{{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-01-debug/output.txt:9:10}}

Rust “确实” 包含了打印调试信息的功能,但我们必须显式选择加入才能为我们的结构体提供该功能。为此,我们在结构体定义之前添加外部属性 #[derive(Debug)],如示例 5-12 所示。

Rust does include functionality to print out debugging information, but we have to explicitly opt in to make that functionality available for our struct. To do that, we add the outer attribute #[derive(Debug)] just before the struct definition, as shown in Listing 5-12.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/listing-05-12/src/main.rs}}
}

现在当我们运行程序时,不会收到任何错误,并且会看到以下输出:

Now when we run the program, we won’t get any errors, and we’ll see the following output:

{{#include ../listings/ch05-using-structs-to-structure-related-data/listing-05-12/output.txt}}

太棒了!虽然不是最漂亮的输出,但它显示了该实例所有字段的值,这绝对有助于调试。当我们有更大的结构体时,让输出更容易阅读一些会很有用;在这些情况下,我们可以在 println! 字符串中使用 {:#?} 代替 {:?}。在这个例子中,使用 {:#?} 风格将输出以下内容:

Nice! It’s not the prettiest output, but it shows the values of all the fields for this instance, which would definitely help during debugging. When we have larger structs, it’s useful to have output that’s a bit easier to read; in those cases, we can use {:#?} instead of {:?} in the println! string. In this example, using the {:#?} style will output the following:

{{#include ../listings/ch05-using-structs-to-structure-related-data/output-only-02-pretty-debug/output.txt}}

使用 Debug 格式打印值的另一种方法是使用 dbg!,它接收表达式的所有权(与接收引用的 println! 不同),打印代码中 dbg! 宏调用发生的文件和行号以及该表达式的结果值,并返回该值的所有权。

Another way to print out a value using the Debug format is to use the dbg! macro, which takes ownership of an expression (as opposed to println!, which takes a reference), prints the file and line number of where that dbg! macro call occurs in your code along with the resultant value of that expression, and returns ownership of the value.

注意:调用 dbg! 宏会打印到标准错误控制台流 (stderr),而 println! 则打印到标准输出控制台流 (stdout)。我们将在第 12 章的“将错误重定向到标准错误”部分中更多地讨论 stderrstdout

Note: Calling the dbg! macro prints to the standard error console stream (stderr), as opposed to println!, which prints to the standard output console stream (stdout). We’ll talk more about stderr and stdout in the “Redirecting Errors to Standard Error” section in Chapter 12.

这里有一个例子,我们对分配给 width 字段的值以及 rect1 中整个结构体的值感兴趣:

Here’s an example where we’re interested in the value that gets assigned to the width field, as well as the value of the whole struct in rect1:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-05-dbg-macro/src/main.rs}}
}

我们可以将 dbg! 放在表达式 30 * scale 周围,由于 dbg! 会返回表达式值的所有权,width 字段将获得与我们不使用 dbg! 调用时相同的值。我们不希望 dbg! 获取 rect1 的所有权,所以我们在下一次调用中使用 rect1 的引用。以下是该示例的输出结果:

{{#include ../listings/ch05-using-structs-to-structure-related-data/no-listing-05-dbg-macro/output.txt}}

我们可以看到第一部分输出源自 src/main.rs 第 10 行,我们正在调试表达式 30 * scale ,其结果值为 60(为整数实现的 Debug 格式是仅打印其值)。在 src/main.rs 第 14 行调用的 dbg! 输出 &rect1 的值,即 Rectangle 结构体。此输出使用了 Rectangle 类型漂亮的 Debug 格式化。当你试图弄清楚代码在做什么时,dbg! 宏真的非常有帮助!

We can see the first bit of output came from src/main.rs line 10 where we’re debugging the expression 30 * scale, and its resultant value is 60 (the Debug formatting implemented for integers is to print only their value). The dbg! call on line 14 of src/main.rs outputs the value of &rect1, which is the Rectangle struct. This output uses the pretty Debug formatting of the Rectangle type. The dbg! macro can be really helpful when you’re trying to figure out what your code is doing!

除了 Debug 特征之外,Rust 还为我们提供了许多可以与 derive 属性配合使用的特征,从而为我们的自定义类型添加有用的行为。这些特征及其行为列在 附录 C 中。我们将在第 10 章介绍如何使用自定义行为实现这些特征,以及如何创建你自己的特征。除了 derive 之外还有许多其他属性;更多信息请参阅 Rust 参考手册中的 “属性 (Attributes)” 部分

In addition to the Debug trait, Rust has provided a number of traits for us to use with the derive attribute that can add useful behavior to our custom types. Those traits and their behaviors are listed in Appendix C. We’ll cover how to implement these traits with custom behavior as well as how to create your own traits in Chapter 10. There are also many attributes other than derive; for more information, see the “Attributes” section of the Rust Reference.

我们的 area 函数非常具体:它只计算长方形的面积。将此行为与我们的 Rectangle 结构体更紧密地联系起来会很有帮助,因为它不适用于任何其他类型。让我们来看看如何通过将 area 函数转变为定义在 Rectangle 类型上的 area “方法 (method)” 来继续重构代码。

Our area function is very specific: It only computes the area of rectangles. It would be helpful to tie this behavior more closely to our Rectangle struct because it won’t work with any other type. Let’s look at how we can continue to refactor this code by turning the area function into an area method defined on our Rectangle type.