5.2 struct使用例(加打印调试信息)
5.2.1. 例子需求
创建一个函数,计算长方形的面积。长和宽类型均为 u32,面积类型也为 u32。
5.2.2. 普通解法
最简单的解法就是定义这个函数有两个参数:一个宽一个长,都为 &u32 类型(例子中说了值是 u32 类型,并且这个场景下不需要函数获得数据所有权,所以使用引用,在数据类型前加 &)。在函数中返回宽乘以长的值就行。
fn main() {
let width = 30;
let length = 50;
println!("{}", area(&width, &length));
}
fn area(width: &u32, length: &u32) -> u32 {
width * length
}
输出:
1500
5.2.3. 元组解法
普通解法本身没有问题,但在可维护性上有一个问题:宽和长是独立的参数,程序中的任何地方都不清楚这些参数是相关的。将宽度和长度组合在一起会更具可读性,也更易于管理。对于数据的整合,使用元组再好不过(因为都是同一数据类型,所以在这里使用数组也是可以的)。
fn main() {
let rectangle = (30,50);
println!("{}", area(&rectangle));
}
fn area(dim:&(u32,u32)) -> u32 {
dim.0 * dim.1
}
输出:
1500
5.2.4. struct解法
元组解法虽然提升了可维护性,但代码的可读性变差了,因为如果不加注释,没人知道元组的第一个数据是代表宽还是代表长(虽然对于计算面积来说无所谓,但是对于较大的项目来说很重要)。元组的元素是没有名字的,即使是元组结构体(上一篇文章 5.1. 定义并实例化struct 中有讲),它里面的元素也是没有名字的。
那么哪种数据结构可以把两个数据整合到一起并且分别赋名呢?没错,就是 struct。
struct Rectangle {
width: u32,
length: u32,
}
fn main() {
let rectangle = Rectangle{
width: 30,
length: 50,
};
println!("{}", area(&rectangle));
}
fn area(dim:&Rectangle) -> u32 {
dim.width * dim.length
}
5.2.5. 打印结构体的调试信息
接着上面的代码,如果再加一行直接打印 rectangle 这个实例会怎么样呢?代码如下:
struct Rectangle {
width: u32,
length: u32,
}
fn main() {
let rectangle = Rectangle{
width: 30,
length: 50,
};
println!("{}", area(&rectangle));
println!("{}", rectangle); // Print the instance directly
}
fn area(dim:&Rectangle) -> u32 {
dim.width * dim.length
}
输出:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
--> src/main.rs:12:20
|
12 | println!("{}", rectangle);
| -- ^^^^^^^^^ `Rectangle` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Rectangle`
--> src/main.rs:1:1
|
1 | struct Rectangle {
| ^^^^^^^^^^^^^^^^
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
先解释一下报错:println! 这个宏可以执行很多种格式化打印。占位符 {} 就是告诉 println! 来使用 std::fmt::Display 这个 trait(理解成接口),类似于 Python 的 toString。报错信息告诉我们,Rectangle 并没有实现 std::fmt::Display 这个 trait,所以不能这样打印。
实际上,目前所讲的基础数据类型默认都实现了 std::fmt::Display,因为它们的展示方式都比较单一。比如说把 1 打印出来,程序只可能打印出阿拉伯数字 1。但是对于有两个字段的 Rectangle,是要都打印,还是只打印 width,还是只打印 length 呢?可能性太多了,所以 Rust 并没有为 struct 默认实现 std::fmt::Display。
但如果我们继续往下看到这一行:
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
编译器提示我们可以使用 {:?} 或者 {:#?} 来代替 {}。那就试试第一种:
struct Rectangle {
width: u32,
length: u32,
}
fn main() {
let rectangle = Rectangle{
width: 30,
length: 50,
};
println!("{}", area(&rectangle));
println!("{:?}", rectangle); // Change `{}` to `{:?}`
}
fn area(dim:&Rectangle) -> u32 {
dim.width * dim.length
}
还是报错了:
error[E0277]: `Rectangle` doesn't implement `Debug`
--> src/main.rs:12:22
|
12 | println!("{:?}", rectangle);
| ---- ^^^^^^^^^ `Rectangle` cannot be formatted using `{:?}` because it doesn't implement `Debug`
| |
| required by this formatting parameter
|
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
help: consider annotating `Rectangle` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Rectangle {
|
但报错信息变了。上一回是没有实现 std::fmt::Display,这回是没有实现 Debug。Debug 和 Display 一样也是一种格式化方法。继续往下看到 note 这行:
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
编译器提示我们添加 #[derive(Debug)] 到代码中,或手动实现 Debug 这个 trait。这里使用前一种(手动实现 trait 会在后面的章节讲解):
#[derive(Debug)]
struct Rectangle {
width: u32,
length: u32,
}
fn main() {
let rectangle = Rectangle{
width: 30,
length: 50,
};
println!("{}", area(&rectangle));
println!("{:?}", rectangle);
}
fn area(dim:&Rectangle) -> u32 {
dim.width * dim.length
}
输出:
1500
Rectangle { width: 30, length: 50 }
这次就可以成功通过了。Rust 本身包含了打印调试信息的功能,但必须为自己代码中的结构体显式地选择这一功能,所以要在定义结构体前加上 #[derive(Debug)] 这个注解。这种输出把结构体的名字、字段的名字及值都显示出来了。
有的时候结构体里有很多字段,这时 {:?} 打印出的横向排列就不那么易读。如果想要输出更加易读,那就把 {:?} 改为 {:#?}:
#[derive(Debug)]
struct Rectangle {
width: u32,
length: u32,
}
fn main() {
let rectangle = Rectangle{
width: 30,
length: 50,
};
println!("{}", area(&rectangle));
println!("{:#?}", rectangle);
}
fn area(dim:&Rectangle) -> u32 {
dim.width * dim.length
}
输出:
1500
Rectangle {
width: 30,
length: 50,
}
这个输出中字段就是纵向排列,对于有很多字段的结构体来说更加易读。
实际上 Rust 提供了很多 trait 让我们可以进行 derive(派生),这些 trait 可以为自定义类型添加很多功能。所有的 trait 和它们的行为都可以在官方指南中找到,我把网址链接附在这里。
在上边的代码中就是让 Rectangle 这个 struct 派生 Debug 这个 trait,所以在打印时就可以使用调试模式。
再举个例子,假设你有一个表示点坐标的结构体:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point1 = Point { x: 1, y: 2 };
let point2 = point1.clone();
println!("{:?}", point1); // Print Point using the Debug trait
assert_eq!(point1, point2); // Compare two Point values using the PartialEq trait
}
在这个例子中:
#[derive(Debug)]允许你使用{:?}格式化规范来打印Point结构体的实例。#[derive(Clone)]允许你创建一个Point实例的副本。#[derive(PartialEq)]允许你比较两个Point实例是否相等。