From 84aa72bf3a06592735ee8fcd2ce630d13f5af929 Mon Sep 17 00:00:00 2001 From: "rust-lang.xfoss.com" Date: Thu, 30 Nov 2023 14:29:47 +0800 Subject: [PATCH] Re-constructured Ch05. --- ...Using_Structs_to_Structure_Related_Data.md | 688 ------------------ src/SUMMARY.md | 3 + src/structs/defining_and_instantiating.md | 243 +++++++ src/structs/example_program.md | 265 +++++++ src/structs/method_syntax.md | 208 ++++++ 5 files changed, 719 insertions(+), 688 deletions(-) create mode 100644 src/structs/defining_and_instantiating.md create mode 100644 src/structs/example_program.md create mode 100644 src/structs/method_syntax.md diff --git a/src/Ch05_Using_Structs_to_Structure_Related_Data.md b/src/Ch05_Using_Structs_to_Structure_Related_Data.md index 1610fb2..b94ee52 100644 --- a/src/Ch05_Using_Structs_to_Structure_Related_Data.md +++ b/src/Ch05_Using_Structs_to_Structure_Related_Data.md @@ -3,691 +3,3 @@ **Using Structs to Structure Related Data** *结构体(struct)*,或者说 *结构(structure)*,实现了将多个相关值打包在一起,并取个名字,而构成一个有意义的组别。在熟悉面向对象语言的情况下,那么 *结构体* 就像是对象的那些数据属性。在本章中,将把元组与结构体加以比照,从而在既有认识之上,构建出对结构体的认识,并对使用结构体作为一种更佳的数据组织方式的时机,进行演示。这里会对如何定义及初始化结构体进行演示。还会讨论如何定义关联函数,尤其是那种叫做 *方法* 的关联函数,来指明与某个结构体类型相关联的行为。结构体与枚举(将在第 6 章讨论到),这两种数据结构,是充分利用 Rust 的编译时类型检查特性,在程序域中创建新类型的构件。 - -## 结构体的定义及初始化 - -结构体与之前 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节中讨论过的元组数据结构类似,二者都保存着多个相关数据。和元组一样,结构体的各个数据片段可以是不同类型。与原则不同的是,在结构体中将给各个数据片段命名,如此各个值表示什么就清楚了。加上这些名字,就意味着相比于元组更为灵活了:不必为了给某个实例指定他的那些值,或要访问实例的那些值,而对实例数据的顺序有所依赖了。 - -要定义出一个结构体,就要敲入关键字 `struct`,及整个结构体的名字。结构体名字,应对安排在一起的这些数据片段的意义加以描述。随后,就要这一对花括号里头,定义出各个数据片段的名称与类型,这些数据片段,就叫做 *字段(fields)*。比如,下面的清单 5-1 就给出了一个保存用户账号信息的结构体。 - -```rust -struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64 -} -``` - -*清单 5-1:`User` 结构体的定义* - -在定义出了结构体后,要用上这个结构体,就要通过给各个字段指定具体值,创建出那个结构体的 *实例(instance)* 来。通过指明结构的名字,并随后加上包含了 `key: value` 键值对的一对花括号,这样创建出一个实例来。键值对中的那些键,就是那些字段的名字,而其中的那些值,则是打算保存在这些字段中的数据。不必按照在结构体中声明那些字段的顺序,来对这些字段进行指明(we don't have to specify the fields in the same order in which we declared them in the struct)。也就是说,结构体定义就如同该类型的通用模板,而实例则将特定数据填充到那个木板中,从而创建出这个类型的值来。比如,就可如下面清单 5-2 中所展示的那样,声明出一个特定的用户来: - -```rust -fn main() { - let user1 = User { - email: String::from("rust@xfoss.com"), - username: String::from("unisko"), - active: true, - sign_in_count: 1 - }; -} -``` - -*清单 5-2:创建出结构体 `User` 的一个实例来* - -而要从结构体中获取到指定值,就要使用点表示法(`.`)。在要的仅是该用户的电子邮件地址时,就可以在那些要用到这个值的地方,使用 `user1.email` 。而在该实例为可变时,那么就可以通过使用点表示法,进而给特定字段赋值,而对某个值加以修改。下面的清单 5-3 展示了如何来修改某个可变 `User` 实例 `email` 字段中的值。 - -文件名:`src/main.rs` - -```rust -fn main() { - let mut user1 = User { - email: String::from("rust@xfoss.com"), - username: String::from("unisko"), - active: true, - sign_in_count: 1 - }; - - user1.email = String::from("java@xfoss.com"); -} -``` - -*清单 5-3:对某个 `User` 实例中的 `email` 字段进行修改* - -请注意这整个实例必须是可变的;Rust 不允许仅将一些字段标记为可变。与所有表达式一样,可以函数体中最后的表达式形式,构造出结构体的新实例,来隐式地返回那个新实例(as with any expression, we can construct a new instance of the struct as the last expression in the function body to implicity return that new instance)。 - -下面的清单 5-4,展示了一个以给定电子邮件和用户名,返回一个 `User` 实例的 `build_user` 函数。其中的 `active` 字符会得到值 `true`,而那个 `sign_in_count` 则会得到值 `1`。 - -```rust -fn build_user(email: String, username: String) -> User { - User { - email: email, - username: username, - active: true, - sign_in_count: 1, - } -} -``` - -*清单 5-4:一个取得电子邮件和用户名,并返回一个 `User` 实例的 `build_user` 函数* - -将函数参数命名为与结构体字段同样的名字,是有意义,但由此而不得不重复那 `email` 与 `username` 的字段名字与变量,就有点烦人了。在结构体有更多字段时,这样重复各个名字就会变得更加烦人。幸运的是,有种方便的简写法! - - -### 使用字段初始化简写法 - -由于在清单 5-4 中的参数名字与结构体字段名字完全一样,因此就可以 *字段初始化简写(field init shorthand)* 语法,来重写 `build_user` 方法,如此一来,`build_user` 函数在没有 `email` 与 `username` 重复的情况下,也有与之前版本同样的表现,如下清单 5-5 所示: - -```rust -fn build_user(email: String, username: String) -> User { - User { - email, - username, - active: true, - sign_in_count: 1, - } -} -``` - -*清单 5-5:由于 `email` 与 `username` 参数与结构体字段有着同样名字,而使用了字段初始化简写的 `build_user` 函数* - -在这里,正创建一个 `User` 结构体的新实例,该结构体有一个名为 `email` 的字段。这里打算将 `email` 字段的值,设置为 `build_user` 函数的 `email` 参数中的值。由于 `email` 字段与 `email` 参数有着同样的名字,因此只就需写下 `email`,而非 `email: email`。 - - -### 使用结构体更新语法,从其他实例创建出实例 - -创建出包含另一实例绝大部分值,而修改一些值的新实例,通常是有用的做法。而使用 *结构体更新语法(struct update syntax)* 就能做到这点。 - -首先,在下面的清单 5-6 中展示了如何按常规,不使用更新语法的情况下,创建出在 `user2` 中的一个新 `User` 实例。这里给 `email` 设置了一个新的值,而在其他方面,则使用了来自之前在清单 5-1 中创建的 `user1` 的那些同样值。 - -```rust -fn main() { - // --跳过代码-- - - let user2 = User { - active: user1.active, - username: user1.username, - email: String::from("java@xfoss.com"), - sign_in_count: user1.sign_in_count, - }; -} -``` - -*清单 5-6:使用一个 `user1` 的值创建出一个新的 `User` 实例* - -而使用结构体更新语法,就可以较少代码,达成同样效果,如下面的清单 5-7 中所给出的那样。其中的 `..` 语法,指明了未显式设置的其余字段,将有着与所给实例中的字段同样的值。 - -```rust -fn main() { - // --跳过代码-- - - let user2 = User { - email: String::from("java@xfoss.com"), - ..user1 - }; -} -``` - -*清单 5-7:使用结构体更新语法来设置 `User` 实例的 `email` 字段值,而使用来自 `user1` 的其余值* - -清单 5-7 中的代码同样创建了在变量 `user2` 中,一个有着 `email` 的不同值,但有着来自 `user1` 的 `username`、`active` 及 `sign_in_count` 同样值。其中的 `..user1` 必须要在最后,这样来指明全部剩余字段都应从 `user1` 中的相应字段获取值,但对于其他字段值的指定,则可选择所要的任意字段,以任意顺序进行,而不论在结构体定义中这些字段的顺序为何(the `..user1` must come last to specify that any remaining fields should get their values from the corresponding fields in `user1`, but we can choose to specify values for as many fields as we want in any order, regardless of the order of the fields in the struct's definition)。 - -请注意结构体更新语法,像赋值一样使用了 `=`;这是由于结构体更新语法迁移了数据,就跟在之前的 ["变量与数据互动方式:迁移"](Ch04_Understanding_Ownership.md#变量与数据互操作方式之一迁移所有权) 小节中看到的那样。在此示例中,在创建了 `user2` 之后,由于变量 `user1` 中的 `username` 字段中的 `String` 值,已被迁移到 `user2` 中了,因此就再也不能使用变量 `user1` 了。若给到 `user2` 的 `email` 及 `username` 字段都是新的 `String` 值,而因此只使用来自 `user1` 的 `active` 和 `sign_in_count` 值,那么在创建了 `user2` 之后,`user1` 仍将是有效的。因为 `active` 和 `sign_in_count` 的类型,都是实现了 `Copy` 特质的类型,因此就会应用在 [唯栈数据:拷贝](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy) 小节中的行为表现。 - - -### 使用没有命名字段的元组结构体来创建不同的类型 - -**Using Tuple Structs without Named Fields to Create Different Types** - -Rust 还支持看起来像元组的结构体,叫做 *元组结构体(tuple structs)*。元组结构体这一类型,多了类型名称中结构体这一部分所提供的意义,却并没有与各字段相关联的名字;而是,元组结构体他们那些字段的类型。在要给予整个元组一个名字,并令到元组成为不同于其他元组的一种类型,且在如同在常规结构体中那样,给各个字段取名字是多余的等等,在这样的情况下,元组结构体就会有用。 - -要定义一个元组结构体,就要以 `struct` 关键字和该结构体的名字开头,接着是一些在元组中的类型。比如,下面分别定义和使用了两个元组结构体 `Color` 与 `Point`: - -```rust -struct Color(i32, i32, i32); -struct Point(i32, i32, i32); - -fn main() { - let black = Color(0, 0, 0); - let white = Color(255, 255, 255); - let origin = Point(0, 0, 0); -} -``` - -请注意,由于这里的 `black` 与 `origin` 两个值是不同元组结构体的实例,因此他们属于不同类型。尽管结构体里的那些字段有着同样类型,对于所定义每个结构体,都是其自身的类型。比如,某个接收类型 `Color` 参数的函数,就无法接收 `Point` 值做参数,尽管这两种类型都是由三个 `i32` 值构成的。除此之外,元组结构体的实例,与元组表现一样:可将他们解构为三个独立部分,可使用 `.` 后面跟上索引,来访问单独值,等等。 - - -### 没有字段的类单元结构 - -**Unit-Like Structs Without Any Fields** - -还可以定义没有任何字段的结构体!由于这些没有任何字段的结构体,与曾在 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节提到过的单元类型 `()` 表现类似,因此他们叫做 *类单元结构体(unit-like structs)*。当需要在某类型上实现某个特质(trait),却又不希望将任何数据存储在那个类型自身里面时,类单元结构体就就有用(unit-like structs can be useful when you need to implement a trait on some type but don't have any data that you want to store in the type itself)。在第 10 章就会讨论到特质。下面是一个声明和初始化名为 `AlwaysEqual` 的单元结构体的示例: - -```rust -struct AlwaysEqual; - -fn main() { - let subject = AlwaysEqual; -} -``` - -要定义出 `AlwaysEqual`,就要使用 `struct` 关键字、想要的名字,随后一个分号即可。是不需要花括号或圆括号的!随后就可以类似方式,得到一个在 `subject` 变量中的 `AlwaysEqual` 的示例了:使用定义的名字,不带任何花括弧或原括弧。设想稍后就要将此类型的表现,实现为每个 `AlwaysEqual` 的实例,总是等于任何其他类型的每个实例,这样做或许是为测试目的,而要有这样的已知结果(imagine that later we'll implement behavior for this type such that every instance of `AlwaysEqual` is always equal to every instance of any other type, perhaps to have a known result for testing purposes)。对于这样的行为表现,是不需要任何数据的!在第 10 章就会看到怎样定义特质,以及在包括类单元结构体在内的任何类型上,怎样实现特质。 - -> **结构体数据的所有权** -> -> 在前面清单 5-1 中的 `User` 结构体定义里,使用的是带有所有权的 `String` 类型,而非 `&str` 字符串切片类型。由于那里是要该结构体的各个实例拥有他自己的数据,且是要在整个结构体有效期间,实例数据有效,因此那里使用 `String` 类型而非 `&str` 类型就是有意而为之的了。 -> -> 结构体存储到其他变量持有数据的引用,也是可能的,但这样做就需要用到 *生命周期(lifetimes)*,而生命周期则是会在后面的第 10 章会讨论到的一个 Rust 特性。生命周期确保某个结构体引用到的数据,会在该结构体有效期间保持有效。譬如说如同下面这样,在尝试在某个结构体中存储不带生命周期的引用时;这就不会工作: -> -> 文件名:`src/main.rs` - -```rust -struct User { - active: bool, - username: &str, - email: &str, - sign_in_count: u64, -} - -fn main() { - let user1 = User { - email: "someone@example.com", - username: "someusername123", - active: true, - sign_in_count: 1, - }; -} -``` - -> 编译器会抱怨他需要生命周期说明符: - -```console -$ cargo run - Compiling structs_demo v0.1.0 (/home/peng/rust-lang/projects/structs_demo) -error[E0106]: missing lifetime specifier - --> src/main.rs:3:15 - | -3 | username: &str, - | ^ expected named lifetime parameter - | -help: consider introducing a named lifetime parameter - | -1 ~ struct User<'a> { -2 | active: bool, -3 ~ username: &'a str, - | - -error[E0106]: missing lifetime specifier - --> src/main.rs:4:12 - | -4 | email: &str, - | ^ expected named lifetime parameter - | -help: consider introducing a named lifetime parameter - | -1 ~ struct User<'a> { -2 | active: bool, -3 | username: &str, -4 ~ email: &'a str, - | - -For more information about this error, try `rustc --explain E0106`. -error: could not compile `structs_demo` due to 2 previous errors -``` - -> 在第 10 章中,就会讨论怎样来修复这些错误,尔后就可以在结构体中存储引用变量了,而至于现在,则只会使用像是 `String` 这样的具有所有权的类型,而避开使用像是 `&str` 这样的引用,来解决这个问题。 - - -## 一个使用结构体的示例程序 - -为搞明白何时会想要使用结构体,下面就来编写一个计算矩形面积的程序。这里会先从使用单个变量开始,并在随后对这个程序进行重构,直到使用结构体为止。 - -下面就来以 `Cargo` 构造一个名为 `rectangles` 的新二进制项目,该项目将取得以像素指定的矩形宽和高,并计算出该矩形的面积。下面的清单 5-8 给出了一个简短的程序,该程序正是有着在这个项目的 `src/main.rs` 中的做法: - -```rust -fn main() { - let width1 = 30; - let height1 = 50; - - println! ( - "该矩形的面积为 {} 平方像素。", - area(width1, height1) - ); -} - -fn area(width: u32, height: u32) -> u32 { - width * height -} -``` - -*清单 5-8:计算由单独宽和高变量指明的矩形面积* - -现在,使用 `cargo run` 允许这个程序: - -```console -$ cargo run - Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) - Finished dev [unoptimized + debuginfo] target(s) in 0.17s - Running `target/debug/rectangles` -该矩形的面积为 1500 平方像素。 -``` - -这段代码通过以两个边长调用 `area` 函数,而成功计算出了该矩形的面积,不过还可以进一步让这段代码更为清晰已读。 - -这段代码的问题,体现在 `area` 函数签名中: - -```rust -fn area(width: u32, height: u32) -> u32 { -``` - -`area` 函数是要计算某个矩形面积的,但这里编写的该函数,有着两个参数,同时在这个程序中,并未清楚表明那两个参数是有联系的。将宽和高组织在一起,代码就会更具易读性,且更具可管理性。在第 3 章的 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节,就已讨论过一种可能那样做的方式:使用元组。 - - -### 以元组进行重构 - -下面的清单 5-9 给出了使用了元组的另一版本的这个程序。 - -文件名:`src/main.rs` - -```rust -fn main() { - let rect1 = (30, 50); - - println! ( - "该矩形的面积为 {} 平方像素。", - area(rect1) - ); -} - -fn area(dimensions: (u32, u32)) -> u32 { - dimensions.0 * dimensions.1 -} -``` - -*清单 5-9:以一个元组来对矩形的宽和高进行指定* - -一方面,这个程序更好了。元组实现了一些代码结构的加入,且现在传递的只有了一个参数。但在另一方面,这个版本变得更不清楚了:元组不会给他的各个元素命名,因此就不得不索引到该元组的各部分,从而令到这里的计算不那么直观了。 - -将宽和高混合起来对于面积计算并不重要,但在要将这个矩形绘制在屏幕上时,那就会有影响了!那时就必须要记住元组中索引 `0` 的是 `width`,而 `height` 是索引 `1`。这对那些将要使用到这代码的其他人来说,将会更难。由于没有在代码中传达数据的意义,因此现在更易于引入错误。 - -### 以结构体进行重构:加入更多意义 - -这里要使用结构体,通过给数据打上标签,来加入更多意义。可将这里正在使用的元组,以给整体命名,同时还给那些部分命名,而转换成为一个结构体。如下清单 5-10 所示。 - -文件名:`src/main.rs` - -```rust -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - println! ( - "该矩形的面积为 {} 平方像素。", - 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` 签名与函数调用中,使用 `&` 符号的原因。 - -`area` 函数会访问那个 `Rectangle` 实例的 `width` 和 `height` 字段。`area` 的函数签名现在表达的正是这里想要的了:使用 `Rectangle` 的 `width` 和 `height` 字段,计算出他的面积。这就传达出了这里的宽与高是相互关联,同时这样做还给到了这些值描述性的名称,而非使用之前元组的索引 `0` 和 `1` 了。这在代码清晰上得了一分。 - - -### 使用派生特质加入有用功能 - -**Adding Useful Functionality with Derived Traits** - -如果能在调试程序期间打印出 `Rectangle` 的实例,并查看到所有字段的值,那就会派上用场。下面的清单 5-11 尝试了使用之前各章已经用到 [`println!` 宏](https://doc.rust-lang.org/std/macro.println.html)。不过这段代码不会工作。 - -```rust -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - println! ("rect1 为:{}", rect1); -} -``` - -*清单 5-11:尝试打印出一个 `Rectangle` 实例* - -在编译这段代码时,会得到有着以下核心消息的错误: - -```console -error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` -``` - -`println!` 宏可完成许多种类的格式化,而默认情况下,那对花括号告诉 `println!` 的是,要使用名为 `Display` 的格式化操作:即用于最终用户直接消费的输出(the `println!` macro can do many kinds of formatting, and by default, the curly brackets tell `println!` to use formatting known as `Display`: output intended for direct end user consumption)。因为在要将一个 `1` 或其他任何原生类型,展示给用户时,都只有唯一的一种方式,因此,对于至今为止已见到过的那些原生类型来说,默认都是实现了 `Display` 的。而对于结构体来说,由于存在更多的显示可能:是要逗号还是不要?要打印出那对花括号吗?所有字段都要展示出来吗?因此 `println!` 对输出进行格式化的方式,就不那么清楚了。正是因为这种模棱两可,Rust 于是就不尝试猜测代码编写者想要的样子,而结构体也就没有一个事先提供的、与 `println!` 和 `{}` 占位符一起使用的 `Display` 实现了。 - -在继续阅读该错误消息时,就会发现下面这个有用注解: - -```console - = 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 为 {:?}", rect1);`。将说明符 `:?` 放在那对花括号里头,就会告诉 `println!`,这里是要使用一个名为 `Debug` 的输出。而 `Debug` 特质就令到这里可将那个结构体,以对开发者有用的方式打印出来,如此就可以在对代码进行调试时,看到那个结构体的值了。 - -在此改变下,对该代码进行编译。见鬼!还是得到个错误: - -```console -error[E0277]: `Rectangle` doesn't implement `Debug` -``` - -不过编译器再度给到一个帮助性注释: - -```console - = help: the trait `Debug` is not implemented for `Rectangle` - = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` -``` - -Rust *确实* 带有打印输出调试信息的功能,不过这里必须显式地选择上那功能,从而使得那功能对这个结构体可用。而要实现这个目的,就要在紧接着结构体定义之前,加上外层属性 `#[derive(Debug)]`(the outer attribute `#[derive(Debug)`),如下面的清单 5-12 所示。 - -文件名:`src/main.rs` - -```rust -#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - println! ("rect1 为:{:?}", rect1); -} -``` - -*清单 5-12:加入派生 `Debug` 特质的属性,进而运用调试格式化将那个 `Rectangle` 实例打印出来* - -此时在运行这个程序时,就不会收到任何错误了,且会看到下面的输出: - -```console -$ cargo run - Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) - Finished dev [unoptimized + debuginfo] target(s) in 0.20s - Running `target/debug/rectangles` -rect1 为:Rectangle { width: 30, height: 50 } -``` - -很棒!这虽不是最漂亮的输出,但他给出了该实例全部字段的值,这无疑在调试期间会有帮助。在有着较大的结构体时,让输出更容易阅读一点就会有用;对于那些更大结构体的情形,就可在 `println!` 中使用 `{:#?}` 而非 `{:?}`。而在这个示例中,使用 `{:#?}` 样式将输出: - -```console -cargo run - Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) - Finished dev [unoptimized + debuginfo] target(s) in 0.18s - Running `target/debug/rectangles` -rect1 为:Rectangle { - width: 30, - height: 50, -} -``` - -使用 `Debug` 格式化将某个值打印出来的另一种方式,就是使用 [`dbg!` 宏](https://doc.rust-lang.org/std/macro.dbg.html),这个 `dbg!` 宏会占据某个表达式的所有权,而将那个 `dbg!` 宏调用出现在代码中所在的文件与行号,与那个表达式的结果值一并打印出来,同时返回结果值的所有权(another way to print out a value using the [`dbg!` macro](https://doc.rust-lang.org/std/macro.dbg.html), which takes ownership of an expression, prints the file and line number of where that `dbg!` macro call occurs in your code along with the resulting value of that expression, and returns ownership of the value)。 - -> 注意:对 `dbg!` 宏的调用,会打印到标准错误控制台流(the standard error console stream, `stderr`),这与 `println!` 宏打印到标准输出控制台流(the standard output console stream, `stdout`)相反。在第 12 章中的 [将错误消息写到标准错误而非标准输出](Ch12_An_I_O_Project_Building_a_Command_Line_Program.md#把错误消息写到标准错误而非标准输出) 小节,将讲到更多有关 `stderr` 与 `stdout` 的内容。 - -以下是个其中对赋值给 `width` 字段,以及在变量 `rect1` 中的整个结构体的值感兴趣的示例: - -```rust -#[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` 的所有权,因此在下一个对 `dbg!` 的调用中,使用到到 `rect1` 的引用。下面就是这个示例输出的样子: - -```console -cargo run - Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) - Finished dev [unoptimized + debuginfo] target(s) in 0.22s - Running `target/debug/rectangles` -[src/main.rs:11] 30 * scale = 60 -[src/main.rs:15] &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` 属性一起使用的其他特质,这些特质把有用的行为表现,添加到那些定制类型。Rust 提供的那些特质及其行为,在 [附录 C](Ch21_Appendix.md#附录-c派生特质) 小节中有列出。在第 10 章中,就会涉及到怎样去实现这些有着定制行为的特质,以及怎样创建自己的特质。除了 `derive` 之外,同样还有许多别的属性;有关属性的更多信息,请参阅 [Rust 参考手册的 “属性” 小节](https://doc.rust-lang.org/reference/attributes.html)。 - -这里的 `area` 函数,是相当专用的:他只会计算矩形的面积。由于 `area` 方法不会在其他任何类型上工作,因此将此行为与这里的 `Rectangle` 结构体更紧密的联系起来,就会变得有帮助。接下来就要看看,怎样通过将这个 `area` 函数,转变成一个定义在这里的 `Rectangle` 类型上的方法,而继续重构这段代码。 - - -## 方法语法 - -*方法* 与函数类似:是以 `fn` 关键字和一个名称,来声明出方法,方法可以有参数和返回值,同时包含了在某个地方方法被调用时,运行的一些代码。与函数不同的地方在于,方法是在结构体(或者枚举或特质对象,关于枚举即特质对象,将分别在第 6 和 17 章讲到)的语境里面定义的,且方法的首个参数将始终是 `self`,这个 `self` 表示方法被调用的那个结构体实例本身。 - - -### 方法的定义 - -下面就来将那个将一个 `Rectangle` 实例作为参数的 `area` 函数,修改为定义在 `Rectangle` 结构体上的 `area` 方法,如下清单 5-13 所示: - -```rust -#[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! ("该矩形的面积为 {} 平方像素。", - rect1.area() - ); -} -``` - -*清单 5-13:在 `Rectangle` 结构体上定义一个 `area` 方法* - - -为定义 `Rectangle` 上下文中的函数,这里开启了一个 `Rectangle ` 的 `impl` (implementation)代码块。此 `impl` 代码块里头的所有物件,都会与那个 `Rectangle` 类型相关联。随后这里就把原来那个 `area` 函数,移入到这个 `impl` 的花括弧里,并将函数签名中的首个(而在此情形下,也是唯一的)参数,及函数体中的各处,均修改为 `self`。在 `main` 函数,即原先调用 `area` 函数与将 `rect1` 作为参数传递的地方,现在就可以使用 *方法语法* 来调用那个在 `Rectangle` 实例上的 `area` 方法了。方法语法(the method syntax)是在实例之后:添加一个带着方法名字、括号及全部参数的点。 - -在 `area` 的签名中,使用了 `&self` 而不再是 `rectangle: &Rectangle`。这个 `&self` 实际上是 `self: &Self` 的简写。在 `impl` 代码块内部,类型 `Self` 就是该 `impl` 代码块所针对的类型。方法必定有着这么一个名为 `self` 类型为 `Self` 的参数,作为他们的首个参数,因此 Rust 这才允许将首个参数位置上的该参数,简写为只是他的名称 `self`。请注意这里仍然需要在 `self` 简写前使用 `&` 运算符,来表示此方法借用了 `Self` 类型的实例,这就跟 `rectangle: &Rectangle` 一样。方法可以取得 `self` 的所有权的、不可变地借用 `self` 变量,或者可变地借用 `self` 变量,对于方法的其他参数,也是这样的。 - -> `&self` - 不可变借用;`&mut self` 可变借用;`self` - 取得所有权,发生所有权转移,`self` 所指向的内存堆上的值原来的所有值将失效。 - -这里选择了 `&self`,有着与方法版本中使用 `&Rectangle` 有着同样理由:那就是不打算取得所有权,同时只打算读取结构体中的数据,而不打算写入。在作为方法要执行的一部分,要修改方法调用所在实例时,就要使用 `&mut self` 作为首个参数了。通过仅使用 `self` 作为首个参数,而取得实例所有权的情况,就非常少见了;通常在要将 `self` 转换为其他类型的数据,而要在这样的转换之后,阻止其他调用者使用原先的实例时,会用到这样的技巧。 - -使用方法而不是函数的主要原因,除了提供到方法语法及不必在每个方法签名中重复 `self` 的类型外,那就是为了代码的组织了。这里已将可由某个类型实例完成的事情,放在一个 `impl` 代码块中,而不是要那些后来的代码使用者,在这里提供的库的各个地方去找寻 `Rectangle` 的那些能力。 - -请注意可选择给方法一个与结构体字段相同的名字。比如,这里就可以在 `Rectangle` 上定义一个同样命名为 `width` 的方法: - -文件名:`src/main.rs` - -```rust -impl Rectangle { - fn width(&self) -> bool { - self.width > 0 - } -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - if rect1.width() { - println! ("该矩形的宽不为零;他的宽为 {}", rect1.width); - } -} -``` - -这里,就选择了让 `width` 方法,在实例的 `width` 字段中的值大于 `0` 时返回 `true`,在值为 `0` 时返回 `false`:在名称与某个字段相同的方法里面,可将该字段用于任何目的。在 `main` 方法中,当这里在 `rect1.width` 后跟上一对圆括号时,那么 Rust 就明白这里指的是方法 `width`了。而在没有使用一对圆括号时,Rust 就知道那里表示的是字段 `width`。 - -通常,但并非总是这样,在给到方法与某个字段同样名字时,要的是让那个方法返回与其同名字段中的值,而不会去干别的事情。像这样的方法,就叫做 *获取器(getters)*,而 Rust 并未像其他语言所做的那样,自动实现结构体字段的获取器。由于可将字段构造为私有,而将方法构造为公开,而由此实现对作为类型的公开 API一部分的字段的只读访问。在第 7 章中就会讨论到何为公开与私有,以及怎样将字段或方法指定为公开或私有。 - -#### `->` 操作符(the `->` operator)哪去了呢? - -> 在 C 和 C++ 中,方法调用会用到两个操作符:直接调用在对象上的方法时,要用到 `.`,而在对象的指针上调用方法时,则要用 `->` 操作符,这时还先要对该指针解除引用。换句话说,在 `object` 是个指针时,`object -> something()` 是类似于 `(*object) -> something()` 的。 -> Rust 并无 `->` 操作符的等价操作符;相反,Rust 有着一项名为 *自动引用与解引用(automatic referencing and dereferencing)* 的特性。而方法调用就是 Rust 中有着这种行为表现的少数几个地方之一。 -> -> 以下就是该特性的工作原理:在以 `object.something()` 调用某个方法时,Rust 会自动加上 `&`、`&mut` 或 `*`,这样 `object` 就会匹配上该方法的签名。换句话说,下面的语句是一致的: -> -```rust -p1.distance(&p2); -(&p1).distance(&p2); -``` -> -> 第一个语句看起来要清楚不少。由于方法有着明确的接收者 -- 即 `self` 的类型,因此这种自动引用的行为会生效。在给定了接收者和方法名字后,Rust 就可明确地确定出该方式到底是在读取(`&self`)、改变(`&mut self`),或者是在消费(`self`)。Rust 实现了方法接收者的隐式借用这一事实,是为实现所有权系统在编程实践中符合人机交互,而所做努力的较大部分(the fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice)。 - -### 有多个参数的方法 - -下面就来通过在 `Rectangle` 结构体上实现另一个方法,练习一下方法的运用。这次就要 `Rectangle` 的一个实例,去取得另一个 `Rectangle` 的实例,并在第二个 `Rectangle` 完全能放入到 `self` (即第一个 `Rectangle` )里头时返回 `true`;否则这个方法就会返回 `false`。也就是,一旦定义了这个 `can_hold` 方法,就要能够编写下面清单 5-14 中的那个程序。 - -文件名:`src/main.rs` - -```rust -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - let rect2 = Rectangle { - width: 10, - height: 40, - }; - - let rect3 = Rectangle { - width: 60, - height: 45, - }; - - println! ("rect1 可以装下 rect2 吗?{}", rect1.can_hold(&rect2)); - println! ("rect1 可以装下 rect3 吗?{}", rect1.can_hold(&rect3)); -} -``` - -*清单 5-14:对尚未成文的 `can_hold` 方法进行使用* - -由于 `rect2` 的两个边都小于 `rect1` 的两个边,而 `rect3` 的两个边都要长于 `rect1` 的两个边,因此预期的输出将看起来像下面这样: - -```console -rect1 可以装下 rect2 吗?true -rect1 可以装下 rect3 吗?false -``` - -这里知道要定义的是个方法,因此那将会在 `impl Rectangle` 代码块内部。而方法的名称将是 `can_hold`,同时他会取得作为参数的另一 `Rectangle` 值的不可变借用。通过观察调用该方法的代码,就可以得出那个参数的类型了:`rect1.can_hold(&rect2)` 传入的是 `&rect2`,正是到变量 `rect2` 的不可变借用,而 `rect2` 又是 `Rectangle` 的一个实例。由于这里只需要读取 `rect2`(而非写入,那就意味着将需要一个可变借用了),同时这里是想要 `main` 函数保留 `rect2` 的所有权,这样就可以在 `can_hold` 方法调用之后,还可以再度使用 `rect2`,因此这样做是有理由的。`can_hold` 方法的返回值,将是个布尔值,而该方法的实现会检查 `self` 的宽和高,相应地是否都大于另一个 `Rectangle` 的宽和高。下面就把这个新的 `can_hold` 方法,加入到清单 5-13 的 `impl` 代码块,如下清单 5-15 所示。 - -文件名:`src/main.rs` - -```rust -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) || - (self.width > other.height && self.height > other.width) - } -} -``` - -*清单 5-15:对在 `Rectangle` 上的、取另一 `Rectangle` 实例作为参数的 `can_hold` 方法进行实现* - -在以清单 5-14 中的 `main` 函数来运行此代码是,就会得到想要的输出。方法可取得在 `self` 参数之后添加到其签名的多个参数,同时这些参数就像函数中的参数一样生效。 - -### 关联函数(associated functions) - - -由于定义在 `impl` 代码块内部的全部函数,都是与那个在 `impl` 关键字之后命名的类型相关联的,因此他们都叫做 *关联函数(associated functions)*。因为一些关联函数不需要用到该类型的实例,因此可把这些函数定义为不将 `self` 作为首个参数的关联函数(而这样的话,这些函数就不是方法了)。前面就已用到过这样的一个关联函数:`String::from` 函数就是定义在 `String` 类型上的。 - -非方法的关联函数,通常用于将会返回一个该结构体新实例的构造函数。比如,这里就可提供有着一维参数,并将该一维参数同时用作宽和高的这么一个关联函数,如此就令到相比于两次指定同样值,而更容易创建除正方形的 `Rectangle`。 - -文件名:`src/main.rs` - -```rust -impl Rectangle { - fn square(size: u32) -> Rectangle { - Rectangle { - width: size, - height: size, - } - } -} -``` - -要调用这个关联函数,就要使用带有结构体名字的 `::` 语法;`let sq = Rectangle::square(3);` 就是一个示例;该函数是是在那个结构体的命名空间之下的:`::` 语法,同时用于关联函数,与由模组创建出的命名空间。在第 7 章会讨论到 Rust 的模组概念。 - -### 多个 `impl` 代码块 - -所有结构体都允许有多个 `impl` 代码块。比如前面的清单 5-15 就与下面清单 5-16 给出的代码等价,其中各个方法都在各自的 `impl` 代码块中: - -```rust -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) || - (self.width > other.height && self.height > other.width) - } -} -``` - -*清单 5-16:使用多个 `impl` 代码块对清单 5-15 进行重写* - -虽然这里并无将这些方法分开到多个 `impl` 代码块中的理由,不过这样做也是有效的语法。在第 10 章讨论到泛型和特质时,就会看到多个 `impl` 代码块是有用的情形。 - -## 本章小节 - -结构体实现了创建对于特定领域有意义的定制类型。通过运用结构体,就可以将有关联的数据片段相互连接起来,并给各个数据取名字来让代码清晰。在 `impl` 代码块中,可定义与类型关联的函数,而方法则是一类实现了指定结构体实例所拥有行为的关联函数。 - -然而结构体并非能够创建定制类型的唯一方式:加下了就要转向到 Rust 的枚举特性,将另一工具加入到编程工具箱。 diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 0f17506..552f7dc 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -29,6 +29,9 @@ - [切片类型](src/ownership/the_slice_type.md) - [使用结构体来对相关数据进行架构](Ch05_Using_Structs_to_Structure_Related_Data.md) + - [定义并初始化结构体](src/structs/defining_and_instantiating.md) + - [运用结构体的一个示例程序](src/structs/example_program.md) + - [方法语法](src/structs/method_syntax.md) - [枚举与模式匹配](Ch06_Enums_and_Pattern_Matching.md) diff --git a/src/structs/defining_and_instantiating.md b/src/structs/defining_and_instantiating.md new file mode 100644 index 0000000..1e434b3 --- /dev/null +++ b/src/structs/defining_and_instantiating.md @@ -0,0 +1,243 @@ +# 定义并初始化结构体 + +**Defining and Instantiating Structs** + + +结构体与之前 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节中讨论过的元组数据结构类似,二者都保存着多个相关数据。和元组一样,结构体的各个数据片段可以是不同类型。与原则不同的是,在结构体中将给各个数据片段命名,如此各个值表示什么就清楚了。加上这些名字,就意味着相比于元组更为灵活了:不必为了给某个实例指定他的那些值,或要访问实例的那些值,而对实例数据的顺序有所依赖了。 + +要定义出一个结构体,就要敲入关键字 `struct`,及整个结构体的名字。结构体名字,应对安排在一起的这些数据片段的意义加以描述。随后,就要这一对花括号里头,定义出各个数据片段的名称与类型,这些数据片段,就叫做 *字段(fields)*。比如,下面的清单 5-1 就给出了一个保存用户账号信息的结构体。 + +```rust +struct User { + active: bool, + username: String, + email: String, + sign_in_count: u64 +} +``` + +*清单 5-1:`User` 结构体的定义* + +在定义出了结构体后,要用上这个结构体,就要通过给各个字段指定具体值,创建出那个结构体的 *实例(instance)* 来。通过指明结构的名字,并随后加上包含了 `key: value` 键值对的一对花括号,这样创建出一个实例来。键值对中的那些键,就是那些字段的名字,而其中的那些值,则是打算保存在这些字段中的数据。不必按照在结构体中声明那些字段的顺序,来对这些字段进行指明(we don't have to specify the fields in the same order in which we declared them in the struct)。也就是说,结构体定义就如同该类型的通用模板,而实例则将特定数据填充到那个木板中,从而创建出这个类型的值来。比如,就可如下面清单 5-2 中所展示的那样,声明出一个特定的用户来: + +```rust +fn main() { + let user1 = User { + email: String::from("rust@xfoss.com"), + username: String::from("unisko"), + active: true, + sign_in_count: 1 + }; +} +``` + +*清单 5-2:创建出结构体 `User` 的一个实例来* + +而要从结构体中获取到指定值,就要使用点表示法(`.`)。在要的仅是该用户的电子邮件地址时,就可以在那些要用到这个值的地方,使用 `user1.email` 。而在该实例为可变时,那么就可以通过使用点表示法,进而给特定字段赋值,而对某个值加以修改。下面的清单 5-3 展示了如何来修改某个可变 `User` 实例 `email` 字段中的值。 + +文件名:`src/main.rs` + +```rust +fn main() { + let mut user1 = User { + email: String::from("rust@xfoss.com"), + username: String::from("unisko"), + active: true, + sign_in_count: 1 + }; + + user1.email = String::from("java@xfoss.com"); +} +``` + +*清单 5-3:对某个 `User` 实例中的 `email` 字段进行修改* + +请注意这整个实例必须是可变的;Rust 不允许仅将一些字段标记为可变。与所有表达式一样,可以函数体中最后的表达式形式,构造出结构体的新实例,来隐式地返回那个新实例(as with any expression, we can construct a new instance of the struct as the last expression in the function body to implicity return that new instance)。 + +下面的清单 5-4,展示了一个以给定电子邮件和用户名,返回一个 `User` 实例的 `build_user` 函数。其中的 `active` 字符会得到值 `true`,而那个 `sign_in_count` 则会得到值 `1`。 + +```rust +fn build_user(email: String, username: String) -> User { + User { + email: email, + username: username, + active: true, + sign_in_count: 1, + } +} +``` + +*清单 5-4:一个取得电子邮件和用户名,并返回一个 `User` 实例的 `build_user` 函数* + +将函数参数命名为与结构体字段同样的名字,是有意义,但由此而不得不重复那 `email` 与 `username` 的字段名字与变量,就有点烦人了。在结构体有更多字段时,这样重复各个名字就会变得更加烦人。幸运的是,有种方便的简写法! + + +## 运用字段初始化的简写法 + +**Using the Field Init Shorthand** + + +由于在清单 5-4 中的参数名字与结构体字段名字完全一样,因此就可以 *字段初始化简写(field init shorthand)* 语法,来重写 `build_user` 方法,如此一来,`build_user` 函数在没有 `email` 与 `username` 重复的情况下,也有与之前版本同样的表现,如下清单 5-5 所示: + +```rust +fn build_user(email: String, username: String) -> User { + User { + email, + username, + active: true, + sign_in_count: 1, + } +} +``` + +*清单 5-5:由于 `email` 与 `username` 参数与结构体字段有着同样名字,而使用了字段初始化简写的 `build_user` 函数* + +在这里,正创建一个 `User` 结构体的新实例,该结构体有一个名为 `email` 的字段。这里打算将 `email` 字段的值,设置为 `build_user` 函数的 `email` 参数中的值。由于 `email` 字段与 `email` 参数有着同样的名字,因此只就需写下 `email`,而非 `email: email`。 + + +## 使用结构体更新语法,从其他实例创建实例 + +**Creating Instances from Other Instances with Struct Update Syntax** + + +创建出包含另一实例绝大部分值,而修改一些值的新实例,通常是有用的做法。而使用 *结构体更新语法(struct update syntax)* 就能做到这点。 + +首先,在下面的清单 5-6 中展示了如何按常规,不使用更新语法的情况下,创建出在 `user2` 中的一个新 `User` 实例。这里给 `email` 设置了一个新的值,而在其他方面,则使用了来自之前在清单 5-1 中创建的 `user1` 的那些同样值。 + +```rust +fn main() { + // --跳过代码-- + + let user2 = User { + active: user1.active, + username: user1.username, + email: String::from("java@xfoss.com"), + sign_in_count: user1.sign_in_count, + }; +} +``` + +*清单 5-6:使用一个 `user1` 的值创建出一个新的 `User` 实例* + +而使用结构体更新语法,就可以较少代码,达成同样效果,如下面的清单 5-7 中所给出的那样。其中的 `..` 语法,指明了未显式设置的其余字段,将有着与所给实例中的字段同样的值。 + +```rust +fn main() { + // --跳过代码-- + + let user2 = User { + email: String::from("java@xfoss.com"), + ..user1 + }; +} +``` + +*清单 5-7:使用结构体更新语法来设置 `User` 实例的 `email` 字段值,而使用来自 `user1` 的其余值* + +清单 5-7 中的代码同样创建了在变量 `user2` 中,一个有着 `email` 的不同值,但有着来自 `user1` 的 `username`、`active` 及 `sign_in_count` 同样值。其中的 `..user1` 必须要在最后,这样来指明全部剩余字段都应从 `user1` 中的相应字段获取值,但对于其他字段值的指定,则可选择所要的任意字段,以任意顺序进行,而不论在结构体定义中这些字段的顺序为何(the `..user1` must come last to specify that any remaining fields should get their values from the corresponding fields in `user1`, but we can choose to specify values for as many fields as we want in any order, regardless of the order of the fields in the struct's definition)。 + +请注意结构体更新语法,像赋值一样使用了 `=`;这是由于结构体更新语法迁移了数据,就跟在之前的 ["变量与数据互动方式:迁移"](Ch04_Understanding_Ownership.md#变量与数据互操作方式之一迁移所有权) 小节中看到的那样。在此示例中,在创建了 `user2` 之后,由于变量 `user1` 中的 `username` 字段中的 `String` 值,已被迁移到 `user2` 中了,因此就再也不能使用变量 `user1` 了。若给到 `user2` 的 `email` 及 `username` 字段都是新的 `String` 值,而因此只使用来自 `user1` 的 `active` 和 `sign_in_count` 值,那么在创建了 `user2` 之后,`user1` 仍将是有效的。因为 `active` 和 `sign_in_count` 的类型,都是实现了 `Copy` 特质的类型,因此就会应用在 [唯栈数据:拷贝](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy) 小节中的行为表现。 + + +## 使用不带命名字段的元组结构体,来创建不同类型 + +**Using Tuple Structs without Named Fields to Create Different Types** + +Rust 还支持看起来像元组的结构体,叫做 *元组结构体(tuple structs)*。元组结构体这一类型,多了类型名称中结构体这一部分所提供的意义,却并没有与各字段相关联的名字;而是,元组结构体他们那些字段的类型。在要给予整个元组一个名字,并令到元组成为不同于其他元组的一种类型,且在如同在常规结构体中那样,给各个字段取名字是多余的等等,在这样的情况下,元组结构体就会有用。 + +要定义一个元组结构体,就要以 `struct` 关键字和该结构体的名字开头,接着是一些在元组中的类型。比如,下面分别定义和使用了两个元组结构体 `Color` 与 `Point`: + +```rust +struct Color(i32, i32, i32); +struct Point(i32, i32, i32); + +fn main() { + let black = Color(0, 0, 0); + let white = Color(255, 255, 255); + let origin = Point(0, 0, 0); +} +``` + +请注意,由于这里的 `black` 与 `origin` 两个值是不同元组结构体的实例,因此他们属于不同类型。尽管结构体里的那些字段有着同样类型,对于所定义每个结构体,都是其自身的类型。比如,某个接收类型 `Color` 参数的函数,就无法接收 `Point` 值做参数,尽管这两种类型都是由三个 `i32` 值构成的。除此之外,元组结构体的实例,与元组表现一样:可将他们解构为三个独立部分,可使用 `.` 后面跟上索引,来访问单独值,等等。 + + +## 不带任何字段的类单元结构体 + +**Unit-Like Structs Without Any Fields** + + +还可以定义没有任何字段的结构体!由于这些没有任何字段的结构体,与曾在 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节提到过的单元类型 `()` 表现类似,因此他们叫做 *类单元结构体(unit-like structs)*。当需要在某类型上实现某个特质(trait),却又不希望将任何数据存储在那个类型自身里面时,类单元结构体就就有用(unit-like structs can be useful when you need to implement a trait on some type but don't have any data that you want to store in the type itself)。在第 10 章就会讨论到特质。下面是一个声明和初始化名为 `AlwaysEqual` 的单元结构体的示例: + +```rust +struct AlwaysEqual; + +fn main() { + let subject = AlwaysEqual; +} +``` + +要定义出 `AlwaysEqual`,就要使用 `struct` 关键字、想要的名字,随后一个分号即可。是不需要花括号或圆括号的!随后就可以类似方式,得到一个在 `subject` 变量中的 `AlwaysEqual` 的示例了:使用定义的名字,不带任何花括弧或原括弧。设想稍后就要将此类型的表现,实现为每个 `AlwaysEqual` 的实例,总是等于任何其他类型的每个实例,这样做或许是为测试目的,而要有这样的已知结果(imagine that later we'll implement behavior for this type such that every instance of `AlwaysEqual` is always equal to every instance of any other type, perhaps to have a known result for testing purposes)。对于这样的行为表现,是不需要任何数据的!在第 10 章就会看到怎样定义特质,以及在包括类单元结构体在内的任何类型上,怎样实现特质。 + +> **结构体数据的所有权** +> +> 在前面清单 5-1 中的 `User` 结构体定义里,使用的是带有所有权的 `String` 类型,而非 `&str` 字符串切片类型。由于那里是要该结构体的各个实例拥有他自己的数据,且是要在整个结构体有效期间,实例数据有效,因此那里使用 `String` 类型而非 `&str` 类型就是有意而为之的了。 +> +> 结构体存储到其他变量持有数据的引用,也是可能的,但这样做就需要用到 *生命周期(lifetimes)*,而生命周期则是会在后面的第 10 章会讨论到的一个 Rust 特性。生命周期确保某个结构体引用到的数据,会在该结构体有效期间保持有效。譬如说如同下面这样,在尝试在某个结构体中存储不带生命周期的引用时;这就不会工作: +> +> 文件名:`src/main.rs` + +```rust +struct User { + active: bool, + username: &str, + email: &str, + sign_in_count: u64, +} + +fn main() { + let user1 = User { + email: "someone@example.com", + username: "someusername123", + active: true, + sign_in_count: 1, + }; +} +``` + +> 编译器会抱怨他需要生命周期说明符: + +```console +$ cargo run + Compiling structs_demo v0.1.0 (/home/peng/rust-lang/projects/structs_demo) +error[E0106]: missing lifetime specifier + --> src/main.rs:3:15 + | +3 | username: &str, + | ^ expected named lifetime parameter + | +help: consider introducing a named lifetime parameter + | +1 ~ struct User<'a> { +2 | active: bool, +3 ~ username: &'a str, + | + +error[E0106]: missing lifetime specifier + --> src/main.rs:4:12 + | +4 | email: &str, + | ^ expected named lifetime parameter + | +help: consider introducing a named lifetime parameter + | +1 ~ struct User<'a> { +2 | active: bool, +3 | username: &str, +4 ~ email: &'a str, + | + +For more information about this error, try `rustc --explain E0106`. +error: could not compile `structs_demo` due to 2 previous errors +``` + +> 在第 10 章中,就会讨论怎样来修复这些错误,尔后就可以在结构体中存储引用变量了,而至于现在,则只会使用像是 `String` 这样的具有所有权的类型,而避开使用像是 `&str` 这样的引用,来解决这个问题。 diff --git a/src/structs/example_program.md b/src/structs/example_program.md new file mode 100644 index 0000000..d1f7fae --- /dev/null +++ b/src/structs/example_program.md @@ -0,0 +1,265 @@ +# 一个使用结构体的示例程序 + +为搞明白何时会想要使用结构体,下面就来编写一个计算矩形面积的程序。这里会先从使用单个变量开始,并在随后对这个程序进行重构,直到使用结构体为止。 + +下面就来以 `Cargo` 构造一个名为 `rectangles` 的新二进制项目,该项目将取得以像素指定的矩形宽和高,并计算出该矩形的面积。下面的清单 5-8 给出了一个简短的程序,该程序正是有着在这个项目的 `src/main.rs` 中的做法: + +```rust +fn main() { + let width1 = 30; + let height1 = 50; + + println! ( + "该矩形的面积为 {} 平方像素。", + area(width1, height1) + ); +} + +fn area(width: u32, height: u32) -> u32 { + width * height +} +``` + +*清单 5-8:计算由单独宽和高变量指明的矩形面积* + +现在,使用 `cargo run` 允许这个程序: + +```console +$ cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.17s + Running `target/debug/rectangles` +该矩形的面积为 1500 平方像素。 +``` + +这段代码通过以两个边长调用 `area` 函数,而成功计算出了该矩形的面积,不过还可以进一步让这段代码更为清晰已读。 + +这段代码的问题,体现在 `area` 函数签名中: + +```rust +fn area(width: u32, height: u32) -> u32 { +``` + +`area` 函数是要计算某个矩形面积的,但这里编写的该函数,有着两个参数,同时在这个程序中,并未清楚表明那两个参数是有联系的。将宽和高组织在一起,代码就会更具易读性,且更具可管理性。在第 3 章的 [元组类型](Ch03_Common_Programming_Concepts.md#元组类型) 小节,就已讨论过一种可能那样做的方式:使用元组。 + + +## 使用元组重构 + +**Refactoring with Tuples** + + +下面的清单 5-9 给出了使用了元组的另一版本的这个程序。 + +文件名:`src/main.rs` + +```rust +fn main() { + let rect1 = (30, 50); + + println! ( + "该矩形的面积为 {} 平方像素。", + area(rect1) + ); +} + +fn area(dimensions: (u32, u32)) -> u32 { + dimensions.0 * dimensions.1 +} +``` + +*清单 5-9:以一个元组来对矩形的宽和高进行指定* + +一方面,这个程序更好了。元组实现了一些代码结构的加入,且现在传递的只有了一个参数。但在另一方面,这个版本变得更不清楚了:元组不会给他的各个元素命名,因此就不得不索引到该元组的各部分,从而令到这里的计算不那么直观了。 + +将宽和高混合起来对于面积计算并不重要,但在要将这个矩形绘制在屏幕上时,那就会有影响了!那时就必须要记住元组中索引 `0` 的是 `width`,而 `height` 是索引 `1`。这对那些将要使用到这代码的其他人来说,将会更难。由于没有在代码中传达数据的意义,因此现在更易于引入错误。 + + +## 使用结构体重构:加入更多意义 + +**Refactoring with Structs: Adding More Meaning** + + +这里要使用结构体,通过给数据打上标签,来加入更多意义。可将这里正在使用的元组,以给整体命名,同时还给那些部分命名,而转换成为一个结构体。如下清单 5-10 所示。 + +文件名:`src/main.rs` + +```rust +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ( + "该矩形的面积为 {} 平方像素。", + 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` 签名与函数调用中,使用 `&` 符号的原因。 + +`area` 函数会访问那个 `Rectangle` 实例的 `width` 和 `height` 字段。`area` 的函数签名现在表达的正是这里想要的了:使用 `Rectangle` 的 `width` 和 `height` 字段,计算出他的面积。这就传达出了这里的宽与高是相互关联,同时这样做还给到了这些值描述性的名称,而非使用之前元组的索引 `0` 和 `1` 了。这在代码清晰上得了一分。 + + +## 使用派生特质,加入有用功能 + +**Adding Useful Functionality with Derived Traits** + + +如果能在调试程序期间打印出 `Rectangle` 的实例,并查看到所有字段的值,那就会派上用场。下面的清单 5-11 尝试了使用之前各章已经用到 [`println!` 宏](https://doc.rust-lang.org/std/macro.println.html)。不过这段代码不会工作。 + +```rust +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ("rect1 为:{}", rect1); +} +``` + +*清单 5-11:尝试打印出一个 `Rectangle` 实例* + +在编译这段代码时,会得到有着以下核心消息的错误: + +```console +error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` +``` + +`println!` 宏可完成许多种类的格式化,而默认情况下,那对花括号告诉 `println!` 的是,要使用名为 `Display` 的格式化操作:即用于最终用户直接消费的输出(the `println!` macro can do many kinds of formatting, and by default, the curly brackets tell `println!` to use formatting known as `Display`: output intended for direct end user consumption)。因为在要将一个 `1` 或其他任何原生类型,展示给用户时,都只有唯一的一种方式,因此,对于至今为止已见到过的那些原生类型来说,默认都是实现了 `Display` 的。而对于结构体来说,由于存在更多的显示可能:是要逗号还是不要?要打印出那对花括号吗?所有字段都要展示出来吗?因此 `println!` 对输出进行格式化的方式,就不那么清楚了。正是因为这种模棱两可,Rust 于是就不尝试猜测代码编写者想要的样子,而结构体也就没有一个事先提供的、与 `println!` 和 `{}` 占位符一起使用的 `Display` 实现了。 + +在继续阅读该错误消息时,就会发现下面这个有用注解: + +```console + = 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 为 {:?}", rect1);`。将说明符 `:?` 放在那对花括号里头,就会告诉 `println!`,这里是要使用一个名为 `Debug` 的输出。而 `Debug` 特质就令到这里可将那个结构体,以对开发者有用的方式打印出来,如此就可以在对代码进行调试时,看到那个结构体的值了。 + +在此改变下,对该代码进行编译。见鬼!还是得到个错误: + +```console +error[E0277]: `Rectangle` doesn't implement `Debug` +``` + +不过编译器再度给到一个帮助性注释: + +```console + = help: the trait `Debug` is not implemented for `Rectangle` + = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` +``` + +Rust *确实* 带有打印输出调试信息的功能,不过这里必须显式地选择上那功能,从而使得那功能对这个结构体可用。而要实现这个目的,就要在紧接着结构体定义之前,加上外层属性 `#[derive(Debug)]`(the outer attribute `#[derive(Debug)`),如下面的清单 5-12 所示。 + +文件名:`src/main.rs` + +```rust +#[derive(Debug)] +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + println! ("rect1 为:{:?}", rect1); +} +``` + +*清单 5-12:加入派生 `Debug` 特质的属性,进而运用调试格式化将那个 `Rectangle` 实例打印出来* + +此时在运行这个程序时,就不会收到任何错误了,且会看到下面的输出: + +```console +$ cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.20s + Running `target/debug/rectangles` +rect1 为:Rectangle { width: 30, height: 50 } +``` + +很棒!这虽不是最漂亮的输出,但他给出了该实例全部字段的值,这无疑在调试期间会有帮助。在有着较大的结构体时,让输出更容易阅读一点就会有用;对于那些更大结构体的情形,就可在 `println!` 中使用 `{:#?}` 而非 `{:?}`。而在这个示例中,使用 `{:#?}` 样式将输出: + +```console +cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.18s + Running `target/debug/rectangles` +rect1 为:Rectangle { + width: 30, + height: 50, +} +``` + +使用 `Debug` 格式化将某个值打印出来的另一种方式,就是使用 [`dbg!` 宏](https://doc.rust-lang.org/std/macro.dbg.html),这个 `dbg!` 宏会占据某个表达式的所有权,而将那个 `dbg!` 宏调用出现在代码中所在的文件与行号,与那个表达式的结果值一并打印出来,同时返回结果值的所有权(another way to print out a value using the [`dbg!` macro](https://doc.rust-lang.org/std/macro.dbg.html), which takes ownership of an expression, prints the file and line number of where that `dbg!` macro call occurs in your code along with the resulting value of that expression, and returns ownership of the value)。 + +> 注意:对 `dbg!` 宏的调用,会打印到标准错误控制台流(the standard error console stream, `stderr`),这与 `println!` 宏打印到标准输出控制台流(the standard output console stream, `stdout`)相反。在第 12 章中的 [将错误消息写到标准错误而非标准输出](Ch12_An_I_O_Project_Building_a_Command_Line_Program.md#把错误消息写到标准错误而非标准输出) 小节,将讲到更多有关 `stderr` 与 `stdout` 的内容。 + +以下是个其中对赋值给 `width` 字段,以及在变量 `rect1` 中的整个结构体的值感兴趣的示例: + +```rust +#[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` 的所有权,因此在下一个对 `dbg!` 的调用中,使用到到 `rect1` 的引用。下面就是这个示例输出的样子: + +```console +cargo run + Compiling rectangles v0.1.0 (/home/peng/rust-lang/projects/rectangles) + Finished dev [unoptimized + debuginfo] target(s) in 0.22s + Running `target/debug/rectangles` +[src/main.rs:11] 30 * scale = 60 +[src/main.rs:15] &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` 属性一起使用的其他特质,这些特质把有用的行为表现,添加到那些定制类型。Rust 提供的那些特质及其行为,在 [附录 C](Ch21_Appendix.md#附录-c派生特质) 小节中有列出。在第 10 章中,就会涉及到怎样去实现这些有着定制行为的特质,以及怎样创建自己的特质。除了 `derive` 之外,同样还有许多别的属性;有关属性的更多信息,请参阅 [Rust 参考手册的 “属性” 小节](https://doc.rust-lang.org/reference/attributes.html)。 + +这里的 `area` 函数,是相当专用的:他只会计算矩形的面积。由于 `area` 方法不会在其他任何类型上工作,因此将此行为与这里的 `Rectangle` 结构体更紧密的联系起来,就会变得有帮助。接下来就要看看,怎样通过将这个 `area` 函数,转变成一个定义在这里的 `Rectangle` 类型上的方法,而继续重构这段代码。 + + + diff --git a/src/structs/method_syntax.md b/src/structs/method_syntax.md new file mode 100644 index 0000000..33f9bb9 --- /dev/null +++ b/src/structs/method_syntax.md @@ -0,0 +1,208 @@ +# 方法语法 + +**Method Syntax** + +*方法* 与函数类似:是以 `fn` 关键字和一个名称,来声明出方法,方法可以有参数和返回值,同时包含了在某个地方方法被调用时,运行的一些代码。与函数不同的地方在于,方法是在结构体(或者枚举或特质对象,关于枚举即特质对象,将分别在第 6 和 17 章讲到)的语境里面定义的,且方法的首个参数将始终是 `self`,这个 `self` 表示方法被调用的那个结构体实例本身。 + + +## 定义出方法 + +**Defining Methods** + + +下面就来将那个将一个 `Rectangle` 实例作为参数的 `area` 函数,修改为定义在 `Rectangle` 结构体上的 `area` 方法,如下清单 5-13 所示: + +```rust +#[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! ("该矩形的面积为 {} 平方像素。", + rect1.area() + ); +} +``` + +*清单 5-13:在 `Rectangle` 结构体上定义一个 `area` 方法* + + +为定义 `Rectangle` 上下文中的函数,这里开启了一个 `Rectangle ` 的 `impl` (implementation)代码块。此 `impl` 代码块里头的所有物件,都会与那个 `Rectangle` 类型相关联。随后这里就把原来那个 `area` 函数,移入到这个 `impl` 的花括弧里,并将函数签名中的首个(而在此情形下,也是唯一的)参数,及函数体中的各处,均修改为 `self`。在 `main` 函数,即原先调用 `area` 函数与将 `rect1` 作为参数传递的地方,现在就可以使用 *方法语法* 来调用那个在 `Rectangle` 实例上的 `area` 方法了。方法语法(the method syntax)是在实例之后:添加一个带着方法名字、括号及全部参数的点。 + +在 `area` 的签名中,使用了 `&self` 而不再是 `rectangle: &Rectangle`。这个 `&self` 实际上是 `self: &Self` 的简写。在 `impl` 代码块内部,类型 `Self` 就是该 `impl` 代码块所针对的类型。方法必定有着这么一个名为 `self` 类型为 `Self` 的参数,作为他们的首个参数,因此 Rust 这才允许将首个参数位置上的该参数,简写为只是他的名称 `self`。请注意这里仍然需要在 `self` 简写前使用 `&` 运算符,来表示此方法借用了 `Self` 类型的实例,这就跟 `rectangle: &Rectangle` 一样。方法可以取得 `self` 的所有权的、不可变地借用 `self` 变量,或者可变地借用 `self` 变量,对于方法的其他参数,也是这样的。 + +> `&self` - 不可变借用;`&mut self` 可变借用;`self` - 取得所有权,发生所有权转移,`self` 所指向的内存堆上的值原来的所有值将失效。 + +这里选择了 `&self`,有着与方法版本中使用 `&Rectangle` 有着同样理由:那就是不打算取得所有权,同时只打算读取结构体中的数据,而不打算写入。在作为方法要执行的一部分,要修改方法调用所在实例时,就要使用 `&mut self` 作为首个参数了。通过仅使用 `self` 作为首个参数,而取得实例所有权的情况,就非常少见了;通常在要将 `self` 转换为其他类型的数据,而要在这样的转换之后,阻止其他调用者使用原先的实例时,会用到这样的技巧。 + +使用方法而不是函数的主要原因,除了提供到方法语法及不必在每个方法签名中重复 `self` 的类型外,那就是为了代码的组织了。这里已将可由某个类型实例完成的事情,放在一个 `impl` 代码块中,而不是要那些后来的代码使用者,在这里提供的库的各个地方去找寻 `Rectangle` 的那些能力。 + +请注意可选择给方法一个与结构体字段相同的名字。比如,这里就可以在 `Rectangle` 上定义一个同样命名为 `width` 的方法: + +文件名:`src/main.rs` + +```rust +impl Rectangle { + fn width(&self) -> bool { + self.width > 0 + } +} + +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + if rect1.width() { + println! ("该矩形的宽不为零;他的宽为 {}", rect1.width); + } +} +``` + +这里,就选择了让 `width` 方法,在实例的 `width` 字段中的值大于 `0` 时返回 `true`,在值为 `0` 时返回 `false`:在名称与某个字段相同的方法里面,可将该字段用于任何目的。在 `main` 方法中,当这里在 `rect1.width` 后跟上一对圆括号时,那么 Rust 就明白这里指的是方法 `width`了。而在没有使用一对圆括号时,Rust 就知道那里表示的是字段 `width`。 + +通常,但并非总是这样,在给到方法与某个字段同样名字时,要的是让那个方法返回与其同名字段中的值,而不会去干别的事情。像这样的方法,就叫做 *获取器(getters)*,而 Rust 并未像其他语言所做的那样,自动实现结构体字段的获取器。由于可将字段构造为私有,而将方法构造为公开,而由此实现对作为类型的公开 API一部分的字段的只读访问。在第 7 章中就会讨论到何为公开与私有,以及怎样将字段或方法指定为公开或私有。 + +> **`->` 操作符(the `->` operator)哪去了呢?** +> +> 在 C 和 C++ 中,方法调用会用到两个操作符:直接调用在对象上的方法时,要用到 `.`,而在对象的指针上调用方法时,则要用 `->` 操作符,这时还先要对该指针解除引用。换句话说,在 `object` 是个指针时,`object -> something()` 是类似于 `(*object) -> something()` 的。 +> Rust 并无 `->` 操作符的等价操作符;相反,Rust 有着一项名为 *自动引用与解引用(automatic referencing and dereferencing)* 的特性。而方法调用就是 Rust 中有着这种行为表现的少数几个地方之一。 +> +> 以下就是该特性的工作原理:在以 `object.something()` 调用某个方法时,Rust 会自动加上 `&`、`&mut` 或 `*`,这样 `object` 就会匹配上该方法的签名。换句话说,下面的语句是一致的: +> +```rust +p1.distance(&p2); +(&p1).distance(&p2); +``` +> +> 第一个语句看起来要清楚不少。由于方法有着明确的接收者 -- 即 `self` 的类型,因此这种自动引用的行为会生效。在给定了接收者和方法名字后,Rust 就可明确地确定出该方式到底是在读取(`&self`)、改变(`&mut self`),或者是在消费(`self`)。Rust 实现了方法接收者的隐式借用这一事实,是为实现所有权系统在编程实践中符合人机交互,而所做努力的较大部分(the fact that Rust makes borrowing implicit for method receivers is a big part of making ownership ergonomic in practice)。 + +## 有着更多参数的方法 + +下面就来通过在 `Rectangle` 结构体上实现另一个方法,练习一下方法的运用。这次就要 `Rectangle` 的一个实例,去取得另一个 `Rectangle` 的实例,并在第二个 `Rectangle` 完全能放入到 `self` (即第一个 `Rectangle` )里头时返回 `true`;否则这个方法就会返回 `false`。也就是,一旦定义了这个 `can_hold` 方法,就要能够编写下面清单 5-14 中的那个程序。 + +文件名:`src/main.rs` + +```rust +fn main() { + let rect1 = Rectangle { + width: 30, + height: 50, + }; + + let rect2 = Rectangle { + width: 10, + height: 40, + }; + + let rect3 = Rectangle { + width: 60, + height: 45, + }; + + println! ("rect1 可以装下 rect2 吗?{}", rect1.can_hold(&rect2)); + println! ("rect1 可以装下 rect3 吗?{}", rect1.can_hold(&rect3)); +} +``` + +*清单 5-14:对尚未成文的 `can_hold` 方法进行使用* + +由于 `rect2` 的两个边都小于 `rect1` 的两个边,而 `rect3` 的两个边都要长于 `rect1` 的两个边,因此预期的输出将看起来像下面这样: + +```console +rect1 可以装下 rect2 吗?true +rect1 可以装下 rect3 吗?false +``` + +这里知道要定义的是个方法,因此那将会在 `impl Rectangle` 代码块内部。而方法的名称将是 `can_hold`,同时他会取得作为参数的另一 `Rectangle` 值的不可变借用。通过观察调用该方法的代码,就可以得出那个参数的类型了:`rect1.can_hold(&rect2)` 传入的是 `&rect2`,正是到变量 `rect2` 的不可变借用,而 `rect2` 又是 `Rectangle` 的一个实例。由于这里只需要读取 `rect2`(而非写入,那就意味着将需要一个可变借用了),同时这里是想要 `main` 函数保留 `rect2` 的所有权,这样就可以在 `can_hold` 方法调用之后,还可以再度使用 `rect2`,因此这样做是有理由的。`can_hold` 方法的返回值,将是个布尔值,而该方法的实现会检查 `self` 的宽和高,相应地是否都大于另一个 `Rectangle` 的宽和高。下面就把这个新的 `can_hold` 方法,加入到清单 5-13 的 `impl` 代码块,如下清单 5-15 所示。 + +文件名:`src/main.rs` + +```rust +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) || + (self.width > other.height && self.height > other.width) + } +} +``` + +*清单 5-15:对在 `Rectangle` 上的、取另一 `Rectangle` 实例作为参数的 `can_hold` 方法进行实现* + +在以清单 5-14 中的 `main` 函数来运行此代码是,就会得到想要的输出。方法可取得在 `self` 参数之后添加到其签名的多个参数,同时这些参数就像函数中的参数一样生效。 + + +## 关联函数 + +**associated functions** + + +由于定义在 `impl` 代码块内部的全部函数,都是与那个在 `impl` 关键字之后命名的类型相关联的,因此他们都叫做 *关联函数(associated functions)*。因为一些关联函数不需要用到该类型的实例,因此可把这些函数定义为不将 `self` 作为首个参数的关联函数(而这样的话,这些函数就不是方法了)。前面就已用到过这样的一个关联函数:`String::from` 函数就是定义在 `String` 类型上的。 + +非方法的关联函数,通常用于将会返回一个该结构体新实例的构造函数。比如,这里就可提供有着一维参数,并将该一维参数同时用作宽和高的这么一个关联函数,如此就令到相比于两次指定同样值,而更容易创建除正方形的 `Rectangle`。 + +文件名:`src/main.rs` + +```rust +impl Rectangle { + fn square(size: u32) -> Rectangle { + Rectangle { + width: size, + height: size, + } + } +} +``` + +要调用这个关联函数,就要使用带有结构体名字的 `::` 语法;`let sq = Rectangle::square(3);` 就是一个示例;该函数是是在那个结构体的命名空间之下的:`::` 语法,同时用于关联函数,与由模组创建出的命名空间。在第 7 章会讨论到 Rust 的模组概念。 + + +## 多个 `impl` 代码块 + +**Multiple `impl` Blocks** + + +所有结构体都允许有多个 `impl` 代码块。比如前面的清单 5-15 就与下面清单 5-16 给出的代码等价,其中各个方法都在各自的 `impl` 代码块中: + +```rust +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) || + (self.width > other.height && self.height > other.width) + } +} +``` + +*清单 5-16:使用多个 `impl` 代码块对清单 5-15 进行重写* + +虽然这里并无将这些方法分开到多个 `impl` 代码块中的理由,不过这样做也是有效的语法。在第 10 章讨论到泛型和特质时,就会看到多个 `impl` 代码块是有用的情形。 + +# 本章小节 + +结构体实现了创建对于特定领域有意义的定制类型。通过运用结构体,就可以将有关联的数据片段相互连接起来,并给各个数据取名字来让代码清晰。在 `impl` 代码块中,可定义与类型关联的函数,而方法则是一类实现了指定结构体实例所拥有行为的关联函数。 + +然而结构体并非能够创建定制类型的唯一方式:加下了就要转向到 Rust 的枚举特性,将另一工具加入到编程工具箱。