使用结构体的示例程序

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

让我们用 Cargo 创建一个名为 rectangles 的新二进制项目,该项目将接受以像素为单位指定的矩形的宽度和高度,并计算矩形的面积。列表 5-8 显示了在项目 src/main.rs 中实现这一功能的一种简短程序。

Filename: 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
}
Listing 5-8: Calculating the area of a rectangle specified by separate width and height variables

现在,使用 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 函数应该计算一个矩形的面积,但我们编写的函数有两个参数,并且在我们的程序中没有任何地方表明这些参数是相关的。将宽度和高度组合在一起会使代码更具可读性和可管理性。我们已经在第 3 章的 “元组类型” 部分讨论了一种可能的方法:使用元组。

使用元组重构

列表 5-9 显示了我们程序的另一个版本,该版本使用了元组。

Filename: 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
}
Listing 5-9: Specifying the width and height of the rectangle with a tuple

在一方面,这个程序更好。元组让我们增加了一些结构,并且我们现在只传递一个参数。但在另一方面,这个版本不太清晰:元组不命名它们的元素,所以我们必须对元组的部分进行索引,使我们的计算变得不那么明显。

将宽度和高度混淆对于面积计算来说无关紧要,但如果我们想在屏幕上绘制矩形,这就会很重要!我们必须记住 width 是元组索引 0height 是元组索引 1。如果其他人要使用我们的代码,他们要弄清楚并记住这一点会更加困难。因为我们没有在代码中传达数据的含义,现在更容易引入错误。

使用结构体重构:增加更多含义

我们使用结构体通过标记数据来增加意义。我们可以将正在使用的元组转换为一个具有整体名称以及各部分名称的结构体,如清单 5-10 所示。

Filename: 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
}
Listing 5-10: Defining a Rectangle struct

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

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

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

通过派生特性添加有用的功能

在调试我们的程序时,能够打印 Rectangle 的实例并查看其所有字段的值会很有帮助。列表 5-11 尝试使用 println!,就像我们在前面的章节中使用的一样。然而,这将不起作用。

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {}", rect1);
}
Listing 5-11: Attempting to print a Rectangle instance

当我们编译这段代码时,我们得到一个带有以下核心信息的错误:

error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

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

如果我们继续阅读错误,我们会发现这个有用的注释:

   = 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 特性使我们能够以对开发者有用的方式打印我们的结构体,这样我们可以在调试代码时看到其值。

编译带有此更改的代码。哎呀!我们仍然得到一个错误:

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 所示。

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {rect1:?}");
}
Listing 5-12: Adding the attribute to derive the Debug trait and printing the Rectangle instance using debug formatting

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

$ 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!,它接受一个表达式的所有权(与 println! 不同,后者接受引用),打印该 dbg! 宏调用在代码中的文件和行号以及该表达式的结果值,并返回值的所有权。

注意:调用 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(为整数实现的Debug格式化仅打印它们的值)。src/main.rs第14行的dbg!调用输出&rect1的值,即Rectangle结构体。此输出使用了Rectangle类型的漂亮Debug格式化。dbg!宏在你试图弄清楚代码在做什么时非常有帮助!

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

我们的 area 函数非常具体:它只计算矩形的面积。 将这种行为更紧密地与我们的 Rectangle 结构体关联起来会很有帮助, 因为它对其他任何类型都无效。让我们看看如何通过将 area 函数转换为定义在 Rectangle 类型上的 area 方法 来继续重构这段代码。