使用结构体的示例程序

RustRustBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

欢迎来到使用结构体的示例程序。本实验是《Rust 程序设计语言》的一部分。你可以在 LabEx 中练习你的 Rust 技能。

在本实验中,我们将编写一个使用结构体来计算矩形面积的程序,对最初使用单独变量表示宽度和高度的代码进行重构。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100396{{"使用结构体的示例程序"}} rust/integer_types -.-> lab-100396{{"使用结构体的示例程序"}} rust/function_syntax -.-> lab-100396{{"使用结构体的示例程序"}} rust/expressions_statements -.-> lab-100396{{"使用结构体的示例程序"}} end

使用结构体的示例程序

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

让我们使用 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:使用单独的宽度和高度变量计算矩形的面积

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

The area of the rectangle is 1500 square pixels.

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

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

fn area(width: u32, height: u32) -> u32 {

area 函数应该计算一个矩形的面积,但我们编写的函数有两个参数,并且在我们的程序中任何地方都不清楚这些参数是相关的。将宽度和高度组合在一起会更具可读性和可管理性。我们已经在“元组类型”中讨论过一种实现方法:使用元组。

使用元组进行重构

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

文件名:src/main.rs

fn main() {
    let rect1 = (30, 50);

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

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

清单 5-9:使用元组指定矩形的宽度和高度

从某种程度上来说,这个程序更好一些。元组让我们增加了一点结构,并且现在我们只传递一个参数[1]。但从另一个角度看,这个版本不太清晰:元组没有为其元素命名,所以我们必须对元组的各个部分进行索引[2],这使得我们的计算不那么明显。

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

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

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

文件名:src/main.rs

1 struct Rectangle {
  2 width: u32,
    height: u32,
}

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

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

4 fn area(rectangle: &Rectangle) -> u32 {
  5 rectangle.width * rectangle.height
}

清单 5-10:定义一个 Rectangle 结构体

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

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

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

使用派生 trait 添加有用的功能

在调试程序时,能够打印 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 不会试图猜测我们想要什么,并且结构体没有为与 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);。在花括号内放入 specifier :? 告诉 println! 我们想要使用一种称为 Debug 的输出格式。Debug trait 使我们能够以对开发者有用的方式打印我们的结构体,这样我们在调试代码时就能看到它的值。

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

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

但同样,编译器给了我们一条有用的提示:

= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` or manually implement `Debug`

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 实例

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

rect1 is Rectangle { width: 30, height: 50 }

很好!这不是最漂亮的输出,但它显示了这个实例所有字段的值,这在调试期间肯定会有帮助。当我们有更大的结构体时,有更易于阅读的输出会很有用;在那些情况下,我们可以在 println! 字符串中使用 {:#?} 而不是 {:?}。在这个例子中,使用 {:#?} 样式将输出如下:

rect1 is Rectangle {
    width: 30,
    height: 50,
}

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

注意:调用 dbg! 宏会打印到标准错误控制台流(stderr),而 println! 会打印到标准输出控制台流(stdout)。我们将在“将错误消息写入标准错误而不是标准输出”中更多地讨论 stderrstdout

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

文件名:src/main.rs

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

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
      1 width: dbg!(30 * scale),
        height: 50,
    };

  2 dbg!(&rect1);
}

我们可以在表达式 30 * scale 周围加上 dbg![1],并且因为 dbg! 返回表达式值的所有权,width 字段将获得与我们不在那里使用 dbg! 调用时相同的值。我们不希望 dbg! 获取 rect1 的所有权,所以在下一次调用中我们对 rect1 使用引用[2]。这个例子的输出如下:

[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

我们可以看到第一部分输出来自[1],在那里我们正在调试表达式 30 * scale,其结果值是 60(为整数实现的 Debug 格式化只是打印它们的值)。在[2]处的 dbg! 调用输出 &rect1 的值,即 Rectangle 结构体。这个输出使用了 Rectangle 类型漂亮的 Debug 格式化。当你试图弄清楚你的代码在做什么时,dbg! 宏真的很有帮助!

除了 Debug trait 之外,Rust 还为我们提供了许多 trait 与 derive 属性一起使用,可以为我们的自定义类型添加有用的行为。这些 trait 及其行为列在附录 C 中。我们将在第 10 章中介绍如何用自定义行为实现这些 trait 以及如何创建自己的 trait。除了 derive 之外还有许多属性;有关更多信息,请参阅 Rust 参考的“属性”部分 https://doc.rust-lang.org/reference/attributes.html

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

总结

恭喜你!你已经完成了“使用结构体的示例程序”实验。你可以在 LabEx 中练习更多实验来提升你的技能。