【Rust学习】08_使用结构体代码示例

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

​# 前言

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

内容

现在让我们使用 Cargo 新建一个叫做 rectangles 的程序,它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积。

基础代码

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
}

运行代码:

/Users/wangyang/.cargo/bin/cargo run --color=always --profile dev --package rectangles --bin rectangles
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.

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

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

area 函数应该计算一个长方形的面积,但我们编写的函数有两个参数,并且在我们的程序中的任何位置都不清楚这些参数是否相关。将 width 和 height 组合在一起会更具可读性和更易于管理,所以我们使用元组(Tuples)来进行重构;

重构代码

使用元组重构

现在让我们来一起看看使用元组(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
}

在某种程度上,这个程序更好。Tuples 让我们添加一些结构,我们现在只传递一个参数。但从另一个方面来说,这个版本就不那么清楚了:元组不命名它们的元素,所以我们必须对元组的各个部分进行索引,使我们的计算不那么明显。

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

使用结构体重构

我们使用结构体通过标记数据来添加含义。我们可以将正在使用的元组转换为一个结构体,该结构体具有整体名称,部分也具有名称,代码如下:

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

我们的 area 函数现在使用一个参数定义,我们将其命名为 rectangle,其类型是 struct Rectangle 实例的不可变借用。我们想要借用结构体而不是获得它的所有权。这样,main保留了其所有权,并可以继续使用rect1,这就是我们在函数签名中使用&的原因,也是我们调用函数的地方。

area 函数访问 Rectangle 实例的 widthheight 字段(请注意,访问借用的结构实例的字段不会移动字段值,这就是您经常看到结构借用的原因)。我们的 area 函数签名现在准确地说明了我们的意思:使用 Rectangle 的 widthheight 字段计算 Rectangle 的面积。这传达了 width 和 height 彼此相关,并且它为值提供了描述性名称,而不是使用 01 的 Tuples 索引值。这是一场清晰的胜利。

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

如果我们在调试程序时能够打印 Rectangle 的实例并查看其所有字段的值,那将非常有用。现在我们来尝试打印一下:

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`
  --> src/main.rs:53:29
   |
53 |     println!("rect1 is {}", rect1);
   |                             ^^^^^ `Rectangle` cannot be formatted with the default formatter
   |
   = 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
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

println! 宏可以执行多种格式设置,默认情况下,大括号指示 println! 使用称为 Display: 输出的格式,供最终用户直接使用。到目前为止,我们所看到的基元类型默认实现 Display,因为只有一种方式可以向用户显示 1 或任何其他基元类型。但是对于结构体,println!应该格式化输出的方式就不那么清楚了,因为有更多的显示可能性:是否需要逗号?是否要打印大括号?是否应显示所有字段?由于这种歧义,Rust 不会尝试猜测我们想要什么,并且结构体没有提供的 Display 实现来与 println!{} 占位符一起使用。

根据上面的提示,现在让我们试试以下操作!println! 宏调用现在将类似于 println!("rect1 is {rect1:?}"); 。将说明符 :? 放在大括号内表示 println!我们想要使用一种称为 Debug 的输出格式。Debug trait 使我们能够以对开发人员有用的方式打印我们的结构体,这样我们就可以在调试代码时看到它的值。

很棒,现在我们得到了另一个错误:

error[E0277]: `Rectangle` doesn't implement `Debug`
  --> src/main.rs:54:24
   |
54 |     println!("rect1 is {rect1:?}");
   |                        ^^^^^^^^^ `Rectangle` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider annotating `Rectangle` with `#[derive(Debug)]`
   |
42 + #[derive(Debug)]
43 | struct Rectangle {
   |

不过我们也得到了有用的信息,Rust 确实包含打印调试信息的功能,但我们必须明确选择使该功能可用于我们的结构体。为此,我们在结构体定义之前添加外部属性 #[derive(Debug)]

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

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

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

现在我们再次尝试运行这个代码,看看有什么结果:

/Users/wangyang/.cargo/bin/cargo run --color=always --profile dev --package rectangles --bin rectangles
warning: fields `width` and `height` are never read
  --> src/main.rs:44:5
   |
43 | struct Rectangle {
   |        --------- fields in this struct
44 |     width: u32,
   |     ^^^^^
45 |     height: u32,
   |     ^^^^^^
   |
   = note: `Rectangle` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis
   = note: `#[warn(dead_code)]` on by default

warning: `rectangles` (bin "rectangles") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Process finished with exit code 0

好的,现在我们正常打印出来了,但是同时我们也收到了一个警告,不过我们的却是定义了但是没有去读取和使用,所以这个警告是正常的,如果不想看到,那该怎么办呢,同样的道理,我们在结构体定义之前添加外部属性 #[allow(dead_code)]

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

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

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

现在再次进行编译:

/Users/wangyang/.cargo/bin/cargo run --color=always --profile dev --package rectangles --bin rectangles
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

Process finished with exit code 0

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

/Users/wangyang/.cargo/bin/cargo run --color=always --profile dev --package rectangles --bin rectangles
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rectangles`
rect1 is Rectangle {
    width: 30,
    height: 50,
}

Process finished with exit code 0

使用 Debug 格式打印出值的另一种方法是使用 dbg!宏,它获取表达式的所有权(与 println!相反,它采用引用),打印该 dbg! 宏调用的文件和行号与该表达式的结果值一起在代码中发生,并返回该值的所有权。

注意:调用 dbg!宏将打印到标准错误控制台流 (stderr),而 println! 将打印到标准输出控制台流 (stdout)。

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

#[derive(Debug)] #[allow(dead_code)]
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 的引用。此示例的输出如下所示:

/Users/wangyang/.cargo/bin/cargo run --color=always --profile dev --package rectangles --bin rectangles
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rectangles`
[src/main.rs:68:16] 30 * scale = 60
[src/main.rs:72:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

我们可以看到输出的第一位来自 src/main.rs 第 68行,我们正在调试表达式 30 * scale,其结果值为 60(为整数实现的 Debug 格式是仅打印它们的值)。src/main.rs 第 72 行的 dbg! 调用输出 &rect1 的值,即 Rectangle 结构。此输出使用 Rectangle 类型的漂亮 Debug 格式。dbg! 宏在你试图弄清楚你的代码在做什么时真的非常有用!

除了 Debug trait 之外,Rust 还为我们提供了许多 trait 供我们使用 derive 属性,这些 trait 可以为我们的自定义类型添加有用的行为。后续我们将介绍如何使用自定义行为实施这些特征,以及如何创建自己的特征。除了 derive 之外,还有许多属性;

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

目录
相关文章
|
6天前
|
存储 Rust 编译器
【Rust学习】07_结构体说明
**struct**或 ***structure***是一种自定义数据类型,允许您命名和包装多个相关的值,从而形成一个有意义的组合。如果您熟悉面向对象的语言,那么**struct**就像对象中的数据属性。在本章中,我们将比较和对比元组与结构体,在您已经知道的基础上,来演示结构体是对数据进行分组的更好方法。
15 1
|
18天前
|
Rust 开发者
揭秘Rust编程:模块与包的终极对决,谁将主宰代码组织的新秩序?
【8月更文挑战第31天】在软件工程中,模块化设计能显著提升代码的可读性、可维护性和可重用性。Rust 作为现代系统编程语言,其模块和包管理机制为开发者提供了强有力的工具来组织代码。本文通过对比模块和包的概念及使用场景,探讨了 Rust 中的最佳实践。
14 2
|
29天前
|
Rust Java C++
30天拿下Rust之结构体
在Rust语言中,结构体是一种用户自定义的数据类型,它允许你将多个相关的值组合成一个单一的类型。结构体是一种复合数据类型,可以用来封装多个不同类型的字段,这些字段可以是基本数据类型、其他结构体、枚举类型等。通过使用结构体,你可以创建更复杂的数据结构,并定义它们的行为。
33 2
|
17天前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
18天前
|
Rust 安全 JavaScript
Rust 和 WebAssembly 搞大事啦!代码在浏览器中运行,这波操作简直逆天!
【8月更文挑战第31天】《Rust 与 WebAssembly:将 Rust 代码运行在浏览器中》介绍了 Rust 和 WebAssembly 的强大结合。Rust 是一门安全高效的编程语言,而 WebAssembly 则是新兴的网页技术标准,两者结合使得 Rust 代码能在浏览器中运行,带来更高的性能和安全性。文章通过示例代码展示了如何将 Rust 函数编译为 WebAssembly 格式并在网页中调用,从而实现复杂高效的应用程序,同时确保了内存安全性和跨平台兼容性,为开发者提供了全新的可能性。
54 0
|
1月前
|
存储 Rust 安全
【Rust学习】06_切片
所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
18 1
|
2月前
|
存储 Rust 安全
【Rust学习】04_所有权
所有权是 Rust 最独特的特性,对语言的其余部分有着深远的影响。它使 Rust 能够在不需要垃圾收集器的情况下保证内存安全,因此了解所有权的运作方式非常重要。在本章中,我们将讨论所有权以及几个相关功能:借用、切片以及 Rust 如何在内存中布局数据。
18 1
|
2月前
|
Rust 编译器
【Rust学习】05_引用和借用
在这章我们将开始学习Rust的引用和借用,它们是Rust中重要的概念,它们允许我们创建可变引用,以及创建不可变引用。
18 0
|
18天前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
35 1
|
30天前
|
Rust 安全 编译器
初探 Rust 语言与环境搭建
Rust 是一门始于2006年的系统编程语言,由Mozilla研究员Graydon Hoare发起,旨在确保内存安全而不牺牲性能。通过所有权、借用和生命周期机制,Rust避免了空指针和数据竞争等问题,简化了并发编程。相较于C/C++,Rust在编译时预防内存错误,提供类似C++的语法和更高的安全性。Rust适用于系统编程、WebAssembly、嵌入式系统和工具开发等领域。其生态系统包括Cargo包管理器和活跃社区。学习资源如"The Book"和"Rust by Example"帮助新手入门。安装Rust可通过Rustup进行,支持跨平台操作。
101 2
初探 Rust 语言与环境搭建