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

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

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.

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
}

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

Now, run this program using 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 函数成功算出了长方形的面积,但我们可以做更多工作来使此代码清晰易读。

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:

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 函数本应计算“一个”长方形的面积,但我们编写的函数有两个参数,并且在我们的程序中没有任何地方清楚地表明这两个参数是相关的。将宽度和高度分组在一起会使代码更具可读性且更易于管理。我们已经在第 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

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

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

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
}

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

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

我们使用结构体通过标记数据来增加意义。我们可以将正在使用的元组转换为结构体,既为整体命名,也为部分命名,如示例 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.

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。在花括号内,我们将字段定义为 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.

通过派生 Trait 增加有用功能

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.

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 不会尝试猜测我们想要什么,并且结构体没有提供用于 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.

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

   |                        |`Rectangle` cannot be formatted with the default formatter
   |                        required by this formatting parameter

让我们试试看!println! 宏调用现在将变成 println!("rect1 is {rect1:?}");。在花括号内放入标识符 :? 告诉 println! 我们想要使用一种名为 Debug 的输出格式。Debug trait 使我们能够以一种对开发人员有用的方式打印我们的结构体,以便我们在调试代码时可以看到它的值。

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.

修改后编译代码。哎呀!我们仍然得到一个错误:

error[E0277]: `Rectangle` doesn't implement `Debug`

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

   |                        required by this formatting parameter
   |

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.

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

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

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

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

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

$ 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! 字符串中使用 {:#?} 代替 {:?}。在这个例子中,使用 {:#?} 风格将输出以下内容:

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:

$ 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!,它会获取表达式的所有权(与 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 中整个结构体的值感兴趣:

#[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(为整数实现的 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 trait 之外,Rust 还为我们提供了许多 trait,供我们与 derive 属性一起使用,以便为我们的自定义类型增加有用的行为。这些 trait 及其行为列在附录 C中。我们将在第 10 章介绍如何使用自定义行为实现这些 trait,以及如何创建你自己的 trait。除了 derive 之外还有许多其他属性;更多信息请参阅 Rust 参考手册的“属性”部分

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 “方法”来继续重构此代码。

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.