上一篇: 04-了解所有权
结构体(struct)是一种自定义数据类型,可以将多个相关值打包命名,组成一个有意义的组。如果你熟悉面向对象的语言,那么结构体就像是对象的数据属性。在本章中,我们将对元组和结构体进行对比,在已有知识的基础上说明结构体是更好的数据分组方式。
我们将演示如何定义和实例化结构体。我们将讨论如何定义关联函数,尤其是称为方法(methods)的关联函数,以指定与结构体类型相关的行为。结构体和枚举是在程序域中创建新类型的构件,可以充分利用 Rust 的编译时类型检查功能。
1. 定义和实例化结构体
结构体与 "元组类型 "一节中讨论的元组类似,都可以保存多个相关值。与元组一样,结构体中的数据也可以是不同的类型。与元组不同的是,在结构体中,我们会为每块数据命名,这样就能清楚地知道这些值的含义。添加这些名称意味着结构体比元组更灵活:您不必依赖数据的顺序来指定或访问实例的值。
要定义结构体,我们需要输入关键字 struct 并为整个结构体命名。结构体的名称应描述被组合在一起的数据块的意义。然后,在大括号内定义数据的名称和类型,我们称之为字段。例如,下列显示了一个存储用户账户信息的结构体。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
要在定义结构体后使用该结构体,我们需要为每个字段指定具体的值,从而创建该结构体的实例。在创建实例时,我们先声明结构体的名称,然后添加包含Key-Value”的大括号,其中:Key是字段的名称,Value是我们要存储在这些字段中的数据。我们不必按照在结构体中声明字段的顺序指定字段。换句话说,结构体的定义就像是该类型的通用模板,而实例则在该模板中填入特定数据以创建该类型的值。下面代码所示声明一个特定的用户:
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
sign_in_count: 1,
email: String::from("someone@example.com"),
};
}
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
};
我们尝试编译它,但会出错:
cargo.exe build
Compiling structType v0.1.0 (E:\rustProj\structType)
error: expected item, found `;`
--> src\main.rs:15:2
|
15 | };
| ^ help: remove this semicolon
|
= help: braced struct declarations are not followed by a semicolon
error: could not compile `structType` (bin "structType") due to previous error
Rust编译器告诉我们,Struct声明类型是,结尾是不需要分号";"
要从结构体中获取特定值,我们使用点符号。例如,要访问该用户的电子邮件地址,我们使用 user1.email 。如果实例是可变的,我们就可以通过使用点符号并将其赋值到特定字段来更改值。下面代码展示了如何更改可变 User 实例的 email 字段中的值。
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
sign_in_count: 1,
email: String::from("someone@example.com"),
};
println!("user:{}'s email:{}", user1.username, user1.email);
user1.email = String::from("anotheremail@exaple.com");
println!("now user:{}'s email:{}", user1.username, user1.email);
}
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
请注意,整个实例必须是可变的;Rust 不允许我们只将某些字段标记为可变。与任何表达式一样,我们可以在函数体的最后一个表达式中构造一个新的结构体实例,从而隐式返回该新实例。
下面代码显示了一个 build_user 函数,该函数用给定的电子邮件和用户名返回一个 User 实例。 active 字段的值为 true ,而 sign_in_count 的值为 1 。
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
将函数参数命名为与结构体字段相同的名称是合理的,但必须重复 email 和 username 字段名称和变量有点乏味。如果结构体有更多的字段,重复每个字段的名称就会更加烦人。幸运的是,有一种简便的方法!
1.1 使用字段初始化速记
上面build_user代码由于参数名和结构体字段名完全相同,我们可以使用字段初始化速记语法重写 build_user ,使其行为完全相同,但没有 username 和 email 的重复,代码如下所示:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
在这里,我们要创建一个 User 结构的新实例,它有一个名为 email 的字段。我们要将 email 字段的值设置为 build_user 函数的 email 参数中的值。由于 email 字段和 email 参数的名称相同,我们只需写入 email 而不是 email: email 。
1.2 使用结构更新语法从其他实例创建实例
创建一个结构体的新实例,其中包含另一个实例中的大部分值,但要更改其中的一些值,这通常很有用。您可以使用 struct update 语法来实现这一功能。
首先,下列代码展示了如何在 user2 中定期创建一个新的 User 实例,而不使用更新语法。我们为 email 设置了一个新值,但使用了之前代码中创建的 user1 中的相同值。
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
sign_in_count: 1,
email: String::from("someone@example.com"),
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
如下代码所示,使用结构体更新语法,我们可以用较少的代码实现相同的效果。语法 .. 规定,未明确设置的其余字段应与给定实例中的字段具有相同的值。
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
sign_in_count: 1,
email: String::from("someone@example.com"),
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
上述代码还在 user2 中创建了一个实例,该实例的 email 值不同,但与 user1 中的 username 、 active 和 sign_in_count 字段的值相同。 ..user1 必须放在最后,以指定其余字段应从 user1 中的相应字段获取值,但我们可以选择以任意顺序为任意多个字段指定值,与结构体定义中的字段顺序无关。
请注意,结构更新语法像赋值一样使用 ,这是因为它移动了数据。在上述例中,创建 user2 后,我们不能再将 user1 作为一个整体来使用,因为 user1 的 username 字段中的 String 被移动到了 user2 中。如果我们为 user2 的 email 和 username 都赋予新的 String 值,从而只使用 user1 中的 active 和 sign_in_count 值,那么 user1 在创建 user2 后仍然有效。 active 和 sign_in_count 都是实现了 Copy 特性的类型。
1.3 使用无命名字段的元组结构创建不同类型
Rust 还支持与元组相似的结构体,称为元组结构体。元组结构体具有结构体名称所提供的附加含义,但没有与字段相关联的名称;相反,它们只有字段的类型。如果您想给整个元组一个名称,并使元组与其他元组的类型不同,而且用普通结构体命名每个字段会显得冗长或多余,那么元组结构体就非常有用。
要定义元组结构,首先要使用 struct 关键字和结构名,然后是元组中的类型。例如,我们在这里定义并使用了两个元组结构,分别命名为 Color 和 Point :
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
请注意, black 和 origin 值的类型不同,因为它们是不同元组结构的实例。您定义的每个结构体都有自己的类型,即使结构体中的字段可能具有相同的类型。例如,接收 Color 类型参数的函数不能接收 Point 作为参数,尽管这两种类型都是由三个 i32 值组成的。除此之外,元组结构实例与元组类似,都可以将其重组为单独的部分,并且可以使用,后跟索引来访问单独的值。
1.4 无字段的类单元结构
您还可以定义没有任何字段的结构体,这些结构体被称为类单元结构体(unit-like structs),因为它们的行为类似于我们在 "元组类型 "一节中提到的单元类型 () 。当你需要在某个类型上实现一个特质,但又没有任何数据要存储在类型本身时,类单元结构体就会派上用场。下面是一个名为 AlwaysEqual 的单元结构体的声明和实例化示例:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
定义 AlwaysEqual 时,我们使用 struct 关键字、我们想要的名称,然后使用分号。无需大括号或小括号!然后,我们可以用类似的方法在 subject 变量中获取 AlwaysEqual 的实例:使用我们定义的名称,不需要任何大括号或小括号。想象一下,以后我们将为这种类型实现这样的行为: AlwaysEqual 的每个实例总是等于任何其他类型的每个实例,也许这样做是为了测试目的。我们不需要任何数据来实现这种行为!你将在第 10 章中看到如何定义 traits 并在任何类型上实现它们,包括类单元结构体。
1.5 结构数据的所有权
在User 结构定义中,我们使用了所拥有的 String 类型,而不是 &str 字符串片段类型。这是有意为之,因为我们希望该结构的每个实例都拥有其所有数据,并且只要整个结构有效,这些数据就有效。
结构体也可以存储对其他数据的引用,但这样做需要使用生命周期。生命周期可以确保结构体引用的数据在结构体存在期间一直有效。假设你试图在结构体中存储引用而不指定生命周期,就像下面这样;这是
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
sign_in_count: 1,
email: String::from("someone@example.com"),
};
}
struct User {
active: bool,
username: &String,
email: &String,
sign_in_count: u64,
}
编译器会抱怨说它需要指定生命周期:
cargo.exe build
Compiling structType v0.1.0 (E:\rustProj\structType)
error[E0106]: missing lifetime specifier
--> src\main.rs:12:15
|
12 | username: &String,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
10 ~ struct User<'a> {
11 | active: bool,
12 ~ username: &'a String,
|
error[E0106]: missing lifetime specifier
--> src\main.rs:13:12
|
13 | email: &String,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
10 ~ struct User<'a> {
11 | active: bool,
12 | username: &String,
13 ~ email: &'a String,
|
error[E0308]: mismatched types
--> src\main.rs:4:19
|
4 | username: String::from("someusername123"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&String`, found `String`
|
help: consider borrowing here
|
4 | username: &String::from("someusername123"),
| +
error[E0308]: mismatched types
--> src\main.rs:6:16
|
6 | email: String::from("someone@example.com"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&String`, found `String`
|
help: consider borrowing here
|
6 | email: &String::from("someone@example.com"),
| +
Some errors have detailed explanations: E0106, E0308.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `structType` (bin "structType") due to 4 previous errors
后面章节我们将讨论如何修复这些错误,以便在结构体中存储引用,但现在,我们将使用自有类型(如 String )而不是引用(如 &str )来修复类似错误。
2. 使用结构体的示例程序
为了了解什么情况下我们可能需要使用结构体,让我们编写一个计算矩形面积的程序。我们先使用单变量,然后重构程序,直到使用结构体。
让我们用 Cargo 创建一个名为 rectangles 的新二进制项目,它将以像素为单位指定矩形的宽和高,并计算矩形的面积。下面代码展示了一个简短的程序,其中的一种方法就是在我们项目的 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
}
现在,使用 cargo run 运行该程序:
cargo.exe run
Compiling rectangles v0.1.0 (D:\rustProj\rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.65s
Running `target\debug\rectangles.exe`
The area of the rectangle is 1500 square pixels.
这段代码通过调用 area 函数,成功地计算出了矩形的面积,但我们还可以做得更多,使代码更加清晰易读。
该代码的问题在 area 的签名中显而易见:
fn area(width: u32, height: u32) -> u32 {
area 函数本应计算一个矩形的面积,但我们编写的函数有两个参数,而且程序中也没有明确说明这两个参数之间的关系。如果将宽度和高度组合在一起,会更易于阅读和管理。在之前的章节中,我们已经讨论过一种方法:使用元组。
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
}
从某种意义上说,这个程序更好。元组让我们增加了一些结构,而且我们现在只传递一个参数。但从另一个角度看,这个版本就不那么清晰了:元组没有为其元素命名,因此我们必须为元组的各个部分建立索引,这使得我们的计算不那么明显。
混淆宽度和高度对计算面积没有影响,但如果我们要在屏幕上绘制矩形,就会有影响!我们必须记住 width 是元组索引 0 , height 是元组索引 1 。如果其他人使用我们的代码,就更难理解和牢记这一点了。因为我们没有在代码中传达数据的含义,所以现在更容易引入错误。
2.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 。在大括号中,我们将字段定义为 width 和 height ,这两个字段的类型都是 u32 。然后,在 main 中,我们创建了一个 Rectangle 的特定实例,其宽度为 30 ,高度为 50 。
现在,我们定义的 area 函数只有一个参数,我们将其命名为 rectangle,其类型是不可变的借用结构体 Rectangle实例。因为我们希望借用结构体,而不是获取其所有权。这样, main 就可以保留其所有权并继续使用 rect1 ,这也是我们在函数签名和函数调用中使用 & 的原因。
area 函数访问 Rectangle 实例的 width 和 height 字段(注意,访问借用结构体实例的字段并不会移动字段值,这就是为什么我们经常看到结构体的借用)。现在,我们对 area 的函数签名所表达的意思是:使用 width 和 height 字段计算 Rectangle 的面积。这表达了宽度和高度是相互关联的,并且为这些值提供了描述性的名称,而不是使用 0 和 1 的元组索引值。这在清晰度方面是有优势的。
2.2 利用派生特质添加实用功能
如果能在调试程序时打印 Rectangle 的实例,并查看其所有字段的值,将非常有用。下面代码尝试使用 println! 宏,就像我们在前几章中使用的那样。但这行不通。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {}", rect1);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
当我们编译这段代码时,会出现一个错误,其核心信息是:
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`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
让我们试试看! println! 宏调用现在看起来像 println!("rect1 is {:?}", rect1); 。在大括号中加上 :? 这个说明符,可以告诉 println! 我们要使用一种名为 Debug 的输出格式。 Debug 特质使我们能够以一种对开发人员有用的方式打印结构体,这样我们就可以在调试代码时看到结构体的值。
按此更改编译代码。糟糕!还是出错:
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 确实包含了打印调试信息的功能,但我们必须明确选择将该功能用于我们的结构体。为此,我们在结构体定义之前添加外属性 #[derive(Debug)] ,如下列代码所示:
现在,当我们运行程序时,不会出现任何错误,我们将看到以下输出:
rect1 is Rectangle { width: 30, height: 50 }
不错!这不是最漂亮的输出,但它显示了该实例所有字段的值,这对调试绝对有帮助。当我们使用较大的结构体时,输出结果会更容易阅读;在这种情况下,我们可以在 println! 字符串中使用 {:#?} 代替 {:?} 。在本例中,使用 {:#?} 样式将输出如下内容:
rect1 is Rectangle {
width: 30,
height: 50,
}
使用 Debug 格式打印数值的另一种方法是使用 dbg! 宏,该宏获取表达式的所有权(与 println! 相反,后者获取引用),打印 dbg! 宏调用在代码中出现的文件和行号以及表达式的结果值,并返回数值的所有权。
在下面的示例中,我们关心的是分配给 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.exe run
Compiling rectangles v0.1.0 (D:\rustProj\rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.66s
Running `target\debug\rectangles.exe`
[src\main.rs:10] 30 * scale = 60
[src\main.rs:14] &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 特质之外,Rust 还提供了许多与 derive 属性一起使用的特质,它们可以为我们的自定义类型添加有用的行为。
我们的 area 函数非常特殊:它只计算矩形的面积。将这一行为与我们的 Rectangle 结构更紧密地联系起来会很有帮助,因为它无法与任何其他类型一起工作。让我们看看如何通过将 area 函数转化为定义在 Rectangle 类型上的 area 方法来继续重构这段代码。
3. 方法语法
方法与函数类似:我们使用 fn 关键字和名称来声明方法,方法可以有参数和返回值,方法中包含一些代码,当从其他地方调用方法时,这些代码将被运行。与函数不同的是,方法是在结构体(或枚举或特质对象)的上下文中定义的,其第一个参数始终是 self ,表示方法被调用的结构体的实例。
3.1 定义方法
如下代码所示,让我们改变以 Rectangle 实例为参数的 area 函数,改用定义在 Rectangle 结构上的 area 方法。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
为了在 Rectangle 的上下文中定义函数,我们为 Rectangle 启动了一个 impl (实现)块。 impl 块中的所有内容都将与 Rectangle 类型相关联。然后,我们将 area 函数移入 impl 大括号中,并将签名和正文中的第一个(在本例中也是唯一的)参数改为 self 。在 main 中,我们调用了 area 函数并将 rect1 作为参数传递,而在 Rectangle 实例中,我们可以使用方法语法调用 area 方法。方法语法位于实例之后:我们在方法名称前添加一个点。
在 area 的签名中,我们使用 &self 而不是 rectangle: &Rectangle 。 &self 实际上是 self: &Self 的缩写。在 impl 代码块中,类型 Self 是 impl 代码块的别名。方法的第一个参数必须是一个名为 self 类型为 Self 的参数,因此 Rust 允许你在第一个参数位置只使用名称 self 来缩写它。请注意,我们仍然需要在 self 速记前面使用 & 来表示该方法借用了 Self 实例,就像我们在 rectangle: &Rectangle 中做的那样。方法可以拥有 self 的所有权,也可以不可变借用 self (就像我们在这里所做的),或者可变借用 self ,就像它们可以借用任何其他参数一样。
在这里,我们选择 &self 的原因与在函数版本中使用 &Rectangle 的原因相同:我们不想拥有所有权,我们只想读取结构体中的数据,而不是向其写入数据。如果我们想在调用方法的过程中改变调用方法的实例,我们可以使用 &mut self 作为第一个参数。仅使用 self 作为第一个参数来获取实例所有权的方法并不多见;这种方法通常用于将 self 转换为其他内容,并防止调用者在转换后使用原始实例。
使用方法而不是函数的主要原因,除了提供方法语法和不必在每个方法的签名中重复 self 的类型外,还在于组织。我们把所有能用类型实例做的事情都放在一个 impl 块中,而不是让我们代码的未来用户在我们提供的库中的不同地方搜索 Rectangle 的功能。
请注意,我们可以选择给方法起一个与结构体的某个字段相同的名字。例如,我们可以在 Rectangle 上定义一个方法,并将其命名为 width :
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
在这里,我们选择让 width 方法在实例的 width 字段中的值大于 0 时返回 true ,在 0 时返回 false :我们可以在同名的方法中使用字段来达到任何目的。在 main 中,当我们在 rect1.width 后加上括号时,Rust 知道我们指的是方法 width 。当我们不使用括号时,Rust 知道我们指的是字段 width 。
通常(但不总是),当我们给一个方法起一个与字段相同的名字时,我们希望它只返回字段中的值,而不做其他事情。这样的方法称为获取器,Rust 并不像其他语言那样自动为 struct 字段实现获取器。获取器之所以有用,是因为我们可以将字段私有化,但将方法公有化,这样就可以将字段的只读访问作为类型的公有 API 的一部分。
3.2 带有更多参数的方法
让我们通过在 Rectangle 结构上实现第二个方法来练习使用方法。这一次,我们希望 Rectangle 的实例接收 Rectangle 的另一个实例,如果第二个 Rectangle 可以完全容纳在 self (第一个 Rectangle )中,则返回 true ;否则,返回 false 。也就是说,一旦我们定义了 can_hold 方法,我们就可以编写如下所示的程序。
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
预期输出结果如下,因为 rect2 的两个尺寸都小于 rect1 的尺寸,但 rect3 比 rect1 宽:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
我们知道要定义一个方法,因此它将位于 impl Rectangle 代码块中。方法的名称是 can_hold ,它将使用另一个 Rectangle 的不可变借用作为参数。通过观察调用该方法的代码,我们可以知道参数的类型: rect1.can_hold(&rect2) 将 &rect2 传递给 rect2 ,后者是 Rectangle 的一个不可变借用实例。这是有道理的,因为我们只需要读取 rect2 (而不是写入,这意味着我们需要一个可变借用),而且我们希望 main 保留对 rect2 的所有权,这样我们就可以在调用 can_hold 方法后再次使用它。 can_hold 的返回值将是一个布尔值,其实现将检查 self 的宽度和高度是否分别大于其他 Rectangle 的宽度和高度。
让我们将新的 can_hold 方法添加到 impl 块,如下所示:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
当我们使用 main 函数运行这段代码时,我们将得到所需的输出结果。方法可以接受多个参数,我们可以将这些参数添加到 self 参数之后的签名中,这些参数的作用与函数中的参数相同。
3.3 相关函数
在 impl 代码块中定义的所有函数都称为关联函数,因为它们与以 impl 命名的类型相关联。我们可以定义不以 self 作为第一个参数的关联函数(因此不是方法),因为它们不需要类型的实例来处理。我们已经使用过一个这样的函数:定义在 String 类型上的 String::from 函数。
不是方法的关联函数通常用于返回结构体新实例的构造函数。这些函数通常被称为 new ,但 new 并不是一个特殊的名称,也没有内置在语言中。例如,我们可以选择提供一个名为 square 的关联函数,该函数只有一个维度参数,并将其用作宽度和高度,这样就可以更方便地创建一个正方形 Rectangle ,而不必两次指定相同的值:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
返回类型和函数体中的 Self 关键字是 impl 关键字后出现的类型的别名,在本例中是 Rectangle 。
要调用这个关联函数,我们使用 :: 句法,并加上结构体名称;
let sq = Rectangle::square(3);
就是一个例子。该函数由结构体命名: :: 语法既用于关联函数,也用于模块创建的命名空间。
3.4 多个 impl 块
每个结构体可以有多个 impl 块。例如,下例中每个方法都有自己的 impl 块。
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
这里没有理由将这些方法分隔成多个 impl 块,但这是有效的语法。在后面章节中讨论泛型和特质时,我们将看到多个 impl 块是有用的。
下一篇:06-枚举和模式匹配