键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

使用结构体的示例程序

为了理解何时适合使用结构体,我们来编写一个计算矩形面积的程序。先从使用单独变量开始,然后逐步重构程序,直到改用结构体。

我们用 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 运行此程序:

$ 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 展示了使用元组的另一种程序写法。

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

从一个角度看,这个程序有所改进:元组让数据具备了一些结构,而且我们现在只需传入一个参数。但从另一个角度看,这个版本反而不够清晰:元组的元素没有名称,因此必须通过索引访问各部分,使计算过程不那么直观。

在计算面积时,宽度和高度颠倒并不会造成影响;但如果要在屏幕上绘制矩形,顺序就很重要了!我们必须记住 width 是元组索引 0,而 height 是索引 1。其他人使用我们的代码时,要弄清楚并记住这一点会更困难。由于代码没有表达数据的含义,因此更容易引入错误。

使用结构体重构

结构体通过为数据添加标签来表达其含义。我们可以把正在使用的元组转换为结构体,为整体和各个部分分别命名,如示例 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 结构体

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

现在,area 函数只有一个参数,我们将它命名为 rectangle,其类型是对 Rectangle 结构体实例的不可变借用。正如第 4 章所述,我们希望借用结构体,而不是取得它的所有权。这样 main 仍保留所有权,可以继续使用 rect1;因此我们在函数签名和调用函数时都使用了 &。

area 函数会访问 Rectangle 实例的 width 和 height 字段(注意,访问借用的结构体实例的字段不会移动字段值,因此你经常会看到结构体借用)。现在,area 的函数签名准确表达了我们的意图:使用 Rectangle 的 width 和 height 字段计算面积。这清楚表明了宽度和高度之间的关联,也用描述性名称取代了元组索引 0 和 1。代码因此更加清晰。

使用派生 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 不会猜测我们的意图,因此结构体没有默认实现 Display,不能直接配合 println! 和占位符 {} 使用。

继续查看错误信息,我们会发现这条有用的提示:

help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  --> src/main.rs:1:1

我们来试试!现在,println! 宏调用将写成 println!("rect1 is {rect1:?}");。在花括号中添加格式说明符 :?,会告诉 println! 使用名为 Debug 的输出格式。Debug trait 让我们能以对开发者有用的方式打印结构体,从而在调试代码时查看它的值。

应用此修改后重新编译代码。哎,仍然会得到一个错误:

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

不过,编译器再次给出了有用的提示:

   |                        required by this formatting parameter
   |

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

现在运行程序时不会再出现错误,并且会看到以下输出:

$ 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 章的“将错误重定向到标准错误”一节进一步讨论 stderr 和 stdout。

下面的示例中,我们既想查看赋给 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 trait,Rust 还提供了许多可以与 derive 属性配合使用的 trait,为自定义类型添加实用行为。这些 trait 及其行为列在附录 C中。第 10 章将介绍如何为这些 trait 实现自定义行为,以及如何创建自己的 trait。除了 derive 之外,还有许多其他属性;更多信息请参阅 Rust Reference 中的“属性”一节。

我们的 area 函数用途非常具体:它只能计算矩形的面积。由于它无法用于其他类型,将此行为与 Rectangle 结构体关联起来会更合适。接下来,我们将把 area 函数转换为定义在 Rectangle 类型上的 area 方法,继续重构这段代码。