VS Code 1.63.2
为了理解什么时候可能需要使用结构体,让我们编写一个程序来计算矩形的面积。我们将从单变量开始,然后重构程序,直到使用结构代替。
让我们用`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
}
运行
cargo run
尽管上述示例中可以通过调用每个维度的area函数计算出矩形的面积,但我们可以做得更好。宽度和高度是相互关联的,因为它们一起描述了一个矩形。
这个代码的问题在area的签名中很明显:
fn area(width: u32, height: u32) -> u32 {
面积函数area是用来计算一个矩形的面积的,但是我们写的这个函数有两个参数。这些参数是相关的,但在我们的程序中没有表示。将宽度和高度组合在一起将更易于阅读和管理。我们可以使用Tuple类型来重构上述代码。
可以查看下述代码:
fn main() {
let rect1 = (30, 50); // 使用Tuple类型
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
在某种程度上,这段代码更好。tuple让我们添加一些结构,现在我们只传递一个参数。但是在另一种方式上,这个版本不太清楚:tuple没有命名它们的元素,因此我们的计算变得更加混乱,因为我们必须对元组的各个部分进行索引。
如果我们在计算面积时混合宽度和高度是没有关系的,但如果我们想在屏幕上绘制矩形,这就有关系了!我们必须记住,width是tuple的下标0而height是元组的下标1。如果有其他人在研究这段代码,他们也会发现这一点,并牢记于心。很容易忘记或混淆这些值并导致错误,因为我们没有在代码中表达数据的含义。
我们通过标记数据来使用结构体来增加意义。我们可以将正在使用的tuple转换为具有整体名称和部分名称的数据类型,如下所示:
// 定义Rectangle结构体
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。在花括号内,我们定义了width和height字段,它们的类型都是u32。然后在main中,我们创建了一个特殊的矩形实例,它的宽度为30,高度为50。
我们的area函数现在定义了一个参数,我们将其命名为rectangle,它的类型是一个不可变的借用struct rectangle实例。正如之前的章节所提到的,我们想借用这个结构,而不是占有它。这样,main保留了它的所有权,并可以继续使用rect1,这就是我们在函数签名中使用&的原因以及我们调用函数的地方。
area函数访问Rectangle实例的width和height字段。我们对于area的函数签名现在确切地表达了我们的意思:计算矩形的面积,使用其width和height字段。这表示宽度和高度是相互关联的,它为值提供描述性的名称,而不是使用元组索引值0和1,这样就很清晰了。
当我们重新调试程序并看到它所有字段的值时,能够打印一个Rectangle的实例是很好的。如下示例:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
编译运行
cargo run
println!宏可以执行多种格式,默认情况下,大括号告诉println!使用被称为Display的格式化:直接供终端用户使用的输出。到目前为止,我们所看到的基元类型在默认情况下实现了Display,因为只有一种方式可以向用户显示1或任何其他基元类型。但是对于结构,println!输出的格式应该不那么清晰,因为有更多的显示可能性:是否需要逗号?要打印花括号吗?是否应该显示所有字段?由于这种模糊性,Rust不会尝试猜测我们想要什么,结构也没有提供Display的实现。
可以尝试`println!("rect1 is {:?}", rect1)`这种方式,输入说明符:?在花括号里告诉println!我们希望使用一种名为Debug的输出格式。Debug特性使我们能够以一种对开发人员有用的方式打印结构,这样我们在重新调试代码时就可以看到它的值。更改后的代码如下:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
编译运行
cargo run
Rust确实包含了打印调试信息的功能,但是我们必须明确地选择,使该功能对我们的结构可用。为此,我们在结构体定义前添加外部属性`[derive(Debug)]`,代码如下所示:
// debug属性
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
编译运行
cargo run
好了!这不是最完美的输出,但它显示了这个实例的所有字段的值,这在调试期间肯定会有帮助。当我们有更大的结构体时,有更容易阅读的输出是很有用的。可以使用`{:#?}`代替`{:?}`,代码如下:
// debug属性
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:#?}", rect1);
}
编译运行
cargo run
使用Debug格式打印值的另一种方法是使用dbg!宏。dbg!宏获取表达式的所有权,输出dbg!宏调用与表达式的结果值一起发生在代码中,并返回值的所有权。调用dbg!宏打印到标准错误控制台流(stderr),而不是println!输出到标准输出控制台流(stdout)。这时代码如下:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
编译运行
cargo run
我们可以看到第一个输出来自src/main.rs第489行,其中我们重新调试表达式30 * scale,其结果值为60(为整数实现的Debug格式是只打印它们的值)。dbg!在src/main.rs的493行输出了Rectangle结构体实例&rect1。这个输出使用了Rectangle类型的完美的调试格式。当试图弄清楚代码正在做什么时,dbg!宏非常有用。
除了Debug特性之外,Rust还提供了许多特性,可以与派生属性一起使用,这些派生属性可以为我们的自定义类型添加有用的行为。
area函数非常具体:它只计算矩形的面积。这将有助于把这个行为更紧密地联系到我们的Rectangle结构,因为它不会与任何其他类型工作。让我们看看如何通过将area函数转换为定义在Rectangle类型上的area方法来继续重构这段代码。