使用结构的示例程序

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

让我们用 Cargo 做一个新的二进制项目,叫做 rectangles,它将 以像素为单位指定的矩形的宽度和高度,并计算面积 的矩形。示例 5-8 显示了一个具有 one作方式的简短程序 正是在我们项目的 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
}

该函数应该计算一个矩形的面积,但是 函数有两个参数,在我们的 程序,参数相关。它会更具可读性和更多 易于将 width 和 height 组合在一起。我们已经讨论过一种方法 我们可以在 “The Tuple Type” 一节中这样做 第 3 章:通过使用元组。area

使用 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:使用元组指定矩形的宽度和高度

在某种程度上,这个程序更好。元组让我们添加一点结构,并且 我们现在只传递一个参数。但从另一个方面来说,这个版本更少 clear: Tuples 不命名它们的元素,因此我们必须对 元组,使我们的计算不那么明显。

混合 width 和 height 对于面积计算无关紧要,但如果 我们想在屏幕上绘制矩形,这很重要!我们将不得不 请记住,这是元组索引,是元组 指数。这对其他人来说更难弄清楚并坚持下去 介意他们是否使用我们的代码。因为我们没有传达 我们的数据在我们的代码中,现在更容易引入错误。width0height1

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

我们使用结构体通过标记数据来添加含义。我们可以将元组 我们正在 use into 一个结构体,该结构体具有整体的名称以及 部分,如示例 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

在这里,我们定义了一个结构并将其命名为 .卷曲内部 括号中,我们将字段定义为 和 ,这两个字段都有 类型。然后,在 中,我们创建了一个 的 Width 和 Height 为 的特定实例。Rectanglewidthheightu32mainRectangle3050

我们的函数现在用一个参数定义,我们将其命名为 ,其类型是结构实例的不可变借用。如第 4 章所述,我们想要借用结构体,而不是 拥有它的所有权。这样,保留其所有权并可以继续 using ,这就是我们在函数签名中使用 的原因,而 我们调用函数的位置。arearectangleRectanglemainrect1&

该函数访问实例的 and 字段(请注意,访问借用的结构实例的字段不会 移动字段值,这就是您经常看到 Borrows of Structs) 的原因。我们 函数签名现在准确地说明了我们的意思:计算面积 的 ,使用其 和 字段。这传达了 width 和 height 彼此相关,它为 值,而不是使用 和 的元组索引值。这是一个 赢了才明白。areawidthheightRectangleareaRectanglewidthheight01

使用派生特征添加有用的功能

如果我们能够在 调试我们的程序并查看其所有字段的值。示例 5-11 次尝试 使用我们在 前几章。但是,这行不通。Rectangle

文件名: 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`

宏可以执行多种格式设置,默认情况下,卷曲 方括号指示使用称为 : output intended 的格式 供最终用户直接使用。到目前为止我们见过的基元类型 implementation 的 intent 选项,因为您只想以一种方式显示 a 或任何其他基元类型分配给用户。但是对于结构体,格式化输出的方式就不那么清楚了,因为有更多的 显示可能性:是否需要逗号?是否要打印 大括号?是否应显示所有字段?由于这种歧义,Rust 不会试图猜测我们想要什么,并且结构体没有提供的 实现 to use with 和 placeholder.println!println!DisplayDisplay1println!Displayprintln!{}

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

   = 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!DebugDebug

使用此更改编译代码。妈的!我们仍然收到一个错误:

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 确实包含打印调试信息的功能,但我们 必须显式选择使该功能可用于我们的结构体。 为此,我们在 struct 定义,如示例 5-12 所示。#[derive(Debug)]

文件名: 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:添加属性以派生 trait 并使用调试格式打印实例DebugRectangle

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

$ 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 }

好!这不是最漂亮的输出,但它显示了所有字段的值 对于这个实例,这肯定会在调试过程中有所帮助。当我们有 较大的结构体,拥有更易于阅读的输出很有用;在 这些情况,我们可以在 String 中使用 instead 而不是。在 此示例使用 style 将输出以下内容:{:#?}{:?}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,
}

使用该格式打印出值的另一种方法是使用 dbg!,该宏获取表达式的所有权(与 to ,它需要一个引用),打印 该宏调用在代码中发生的位置以及结果值 ,并返回该值的所有权。Debugprintln!dbg!

注意:调用宏会打印到标准错误控制台流 (),而不是 ,后者打印到标准输出 控制台流 ()。我们将在 “将错误消息写入标准错误而不是标准输出” 中详细讨论 和 第 12 章中的部分dbg!stderrprintln!stdoutstderrstdout

下面是一个示例,我们对分配给字段的值以及 中整个结构体的值感兴趣:widthrect1

#[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 * scaledbg!widthdbg!dbg!rect1rect1

$ 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 行,我们所在的位置 调试表达式 ,其结果值为 (为 Integers 实现的格式是仅打印其值)。src/main.rs 第 14 行的调用输出 的值 ,即 结构体。此输出使用 type 的 pretty 格式。当您尝试 弄清楚你的代码在做什么!30 * scale60Debugdbg!&rect1RectangleDebugRectangledbg!

除了 trait 之外,Rust 还为我们提供了许多 trait 与可以向我们的自定义添加有用行为的属性一起使用 类型。这些特征及其行为在附录 C 中列出。我们将介绍如何使用自定义行为实现这些特征,如 以及如何在第 10 章中创建自己的特征。也有很多 除 ;有关详细信息,请参阅“属性” 部分Debugderivederive

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准