diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md index 7f045e0..6e646a7 100644 --- a/src/ch05-00-structs.md +++ b/src/ch05-00-structs.md @@ -2,6 +2,6 @@ > [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-00-structs.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 55f6c5808a816f2bab0f0a5ad20226c637348c40 -`struct`,是 *structure* 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct` 就像对象中的数据属性(字段)。在本章中,我们会比较元组与结构体,展示如何使用结构体,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。结构体和 **枚举**(*enum*)(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。 +`struct`,或者 *structure*,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct` 就像对象中的数据属性(字段组合)。在本章中,我们会对比元组与结构体的异同,展示如何使用结构体,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。结构体和 **枚举**(*enum*)(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。 diff --git a/src/ch05-01-defining-structs.md b/src/ch05-01-defining-structs.md index d3c56e9..cbe2bfa 100644 --- a/src/ch05-01-defining-structs.md +++ b/src/ch05-01-defining-structs.md @@ -2,7 +2,7 @@ > [ch05-01-defining-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-01-defining-structs.md) >
-> commit 56352c28cf3fe0402fa5a7cba73890e314d720eb +> commit e143d8fca3f914811b1388755ff4d325e9d20cc2 我们在第三章讨论过,结构体与元组类似。就像元组,结构体的每一部分可以是不同类型。不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字使得结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。 @@ -19,7 +19,7 @@ struct User { 示例 5-1:`User` 结构体定义 -一旦定义了结构体后为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 对的形式提供字段,其中 key 是字段的名字,value 是需要储存在字段中的数据值。实例中具体说明字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户: +一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 对的形式提供字段,其中 key 是字段的名字,value 是需要储存在字段中的数据值。实例中具体说明字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户: ```rust # struct User { @@ -39,7 +39,7 @@ let user1 = User { 示例 5-2:创建 `User` 结构体的实例 -为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用 `user1.email`。要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并对对应的字段赋值。示例 5-3 展示了如何改变一个可变的 `User` 实例 `email` 字段的值: +为了从结构体中获取某个特定的值,可以使用点号。如果我们只想要用户的邮箱地址,可以用 `user1.email`。要更改结构体中的值,如果结构体的实例是可变的,我们可以使用点号并为对应的字段赋值。示例 5-3 展示了如何改变一个可变的 `User` 实例 `email` 字段的值: ```rust # struct User { @@ -61,7 +61,9 @@ user1.email = String::from("anotheremail@example.com"); 示例 5-3:改变 `User` 结构体 `email` 字段的值 -与其他任何表达式一样,我们可以在函数体的最后一个表达式构造一个结构体,从函数隐式的返回一个结构体的新实例。表 5-4 显示了一个返回带有给定的 `email` 与 `username` 的 `User` 结构体的实例的 `build_user` 函数。`active` 字段的值为 `true`,并且 `sign_in_count` 的值为 `1`。 +注意整个实例必须是可变的;Rust 并不允许只将特定字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式构造一个结构体,从函数隐式的返回一个结构体的新实例。 + +示例 5-4 显示了一个返回带有给定的 `email` 与 `username` 的 `User` 结构体的实例的 `build_user` 函数。`active` 字段的值为 `true`,并且 `sign_in_count` 的值为 `1`。 ```rust # struct User { @@ -83,13 +85,13 @@ fn build_user(email: String, username: String) -> User { 示例 5-4:`build_user` 函数获取 email 和用户名并返回 `User` 实例 -不过,重复 `email` 字段与 `email` 变量的名字,同样的对于`username`,感觉有一点无趣。将函数参数起与结构体字段相同的名字是可以理解的,但是如果结构体有更多字段,重复它们是十分烦人的。幸运的是,这里有一个方便的语法! +为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 `email` 和 `username` 字段名称与变量有些冗余。如果结构体有更多字段,重复这些名称就显得更加烦人了。幸运的是,有一个方便的简写语法! -### 变量与字段同名时的字段初始化语法 +### 变量与字段同名时的字段初始化简写语法 -如果有变量与字段同名的话,你可以使用 **字段初始化语法**(*field init shorthand*)。这可以让创建新的结构体实例的函数更为简练。 +因为示例 5-4 中的参数名与字段名都完全相同,我们可以使用 **字段初始化简写语法**(*field init shorthand*)来重写 `build_user`,这样其行为与之前完全相同,不过无需重复 `email` 和 `username` 了,如示例 5-5 所示。 -在示例 5-4 中,名为 `email` 与 `username` 的参数与结构体 `User` 的字段 `email` 和 `username` 同名。因为名字相同,我们可以写出不重复 `email` 和 `username` 的 `build_user` 函数,如示例 5-5 所示。 这个版本的函数与示例 5-4 中代码的行为完全相同。这个字段初始化语法可以让这类代码更简洁,特别是当结构体有很多字段的时候。 +如果有变量与字段同名的话,你可以使用 **字段初始化简写语法**(*field init shorthand*)。这可以让创建新的结构体实例的函数更为简练。 ```rust # struct User { @@ -109,11 +111,15 @@ fn build_user(email: String, username: String) -> User { } ``` -示例 5-5:`build_user` 函数使用了字段初始化语法,因为 `email` 和 `username` 参数与结构体字段同名 +示例 5-5:`build_user` 函数使用了字段初始化简写语法,因为 `email` 和 `username` 参数与结构体字段同名 + +这里我们创建了一个新的 `User` 结构体实例,它有一个叫做 `email` 的字段。我们想要将 `email` 字段的值设置为 `build_user` 函数 `email` 参数的值。因为 `email` 字段与 `email` 参数有着相同的名称,则只需编写 `email` 而不是 `email: email`。 ### 使用结构体更新语法从其他对象创建对象 -可以从老的对象创建新的对象常常是很有帮助的,即复用大部分老对象的值并只改变一部分。示例 5-6 展示了一个设置 `email` 与 `username` 的值但其余字段使用与示例 5-2 中 `user1` 实例相同的值以创建新的 `User` 实例 `user2` 的例子: +可以从老的对象创建新的对象常常是很有帮助的,即复用大部分老对象的值并只改变一部分值。这可以通过 **结构体更新语法**(*struct update syntax*)实现。 + +作为开始,示例 5-6 展示了如何不使用更新语法来在 `user2` 中创建一个新 `User` 实例。我们为 `email` 和 `username` 设置了新的值,其他值则使用了实例 5-2 中创建的 `user1` 中的同名值: ```rust # struct User { @@ -138,9 +144,9 @@ let user2 = User { }; ``` -示例 5-6:创建 `User` 新实例,`user2`,并将一些字段的值设置为 `user1` 同名字段的值 +示例 5-6:创建 `User` 新实例,其使用了一些来自 `user1` 的值 -**结构体更新语法**(*struct update syntax*)可以利用更少的代码获得与示例 5-6 相同的效果。结构体更新语法利用 `..` 以指定未显式设置的字段应有与给定实例对应字段相同的值。示例 5-7 中的代码同样地创建了有着不同的 `email` 与 `username` 值但 `active` 和 `sign_in_count` 字段与 `user1` 相同的实例 `user2`: +使用结构体更新语法,我们可以通过更少的代码来达到相同的效果,如示例 5-7 所示。`..` 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。 ```rust # struct User { @@ -164,11 +170,15 @@ let user2 = User { }; ``` -示例 5-7:使用结构体更新语法为 `User` 实例设置新的 `email` 和 `username` 值,但使用 `user1` 变量中剩下字段的值 +示例 5-7:使用结构体更新语法为一个 `User` 实例设置新的 `email` 和 `username` 值,不过其余值来自 `user1` 变量中实例的字段 -### 使用没有命名字段的元组结构体创建不同的类型 +实例 5-7 中的代码也在 `user2` 中创建了一个新实例,其有不同的 `email` 和 `username` 值不过 `active` 和 `sign_in_count` 字段的值与 `user1` 相同。 -也可以定义与元组相像的结构体,称为 **元组结构体**(*tuple structs*),有着结构体名称提供的含义,但没有具体的字段名只有字段的类型。元组结构体的定义仍然以`struct` 关键字与结构体名称,接下来是元组的类型。如以下是命名为 `Color` 与`Point` 的元组结构体的定义与使用: +### 使用没有命名字段的元组结构体来创建不同的类型 + +也可以定义与元组(在第三章讨论过)类似的结构体,称为 **元组结构体**(*tuple structs*),有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。元组结构体在你希望命名整个元组并使其与其他(同样的)元组为不同类型时很有用,这时像常规结构体那样为每个字段命名就显得冗余和形式化了。 + +定义元组结构体以 `struct` 关键字和结构体名开头并后跟元组中的类型。例如,这里是两个分别叫做 `Color` 和 `Point` 元组结构体的定义和用例: ```rust struct Color(i32, i32, i32); @@ -178,7 +188,7 @@ let black = Color(0, 0, 0); let origin = Point(0, 0, 0); ``` -注意 `black` 和 `origin` 变量是不同的类型,因为它们是不同的元组结构体的实例。我们定义的每一个结构体有着自己的类型,即使结构体中的字段有相同的类型。在其他方面,元组结构体类似我们在第三章提到的元组。 +注意 `black` 和 `origin` 值是不同的类型,因为它们是不同的元组结构体的实例。我们定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。例如,一个获取 `Color` 类型参数的函数不能接受 `Point` 作为参数,即便这两个类型都由三个 `i32` 值组成。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 `.` 后跟索引来访问单独的值,等等。 ### 没有任何字段的类单元结构体 @@ -186,7 +196,7 @@ let origin = Point(0, 0, 0); > ## 结构体数据的所有权 > -> 在示例 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。 +> 在示例 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。 > > 可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期**(*lifetimes*),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样: > diff --git a/src/ch05-02-example-structs.md b/src/ch05-02-example-structs.md index a577c93..5dcbb20 100644 --- a/src/ch05-02-example-structs.md +++ b/src/ch05-02-example-structs.md @@ -2,33 +2,33 @@ > [ch05-02-example-structs.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-02-example-structs.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 7bf137c1b8f176638c0a7fa136d2e6bdc1f6e7d3 为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。 -使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。示例 5-2 中是项目的 *src/main.rs* 文件中为此实现的一个小程序: +使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的宽度和高度并计算它的面积。示例 5-8 中是项目的 *src/main.rs* 文件中为此实现的一个小程序: 文件名: src/main.rs ```rust fn main() { - let length1 = 50; let width1 = 30; + let height1 = 50; println!( "The area of the rectangle is {} square pixels.", - area(length1, width1) + area(width1, height1) ); } -fn area(length: u32, width: u32) -> u32 { - length * width +fn area(width: u32, height: u32) -> u32 { + width * height } ``` -示例 5-8:通过指定长方形的长宽变量来计算长方形面积 +示例 5-8:通过分别指定长方形的宽高变量来计算长方形面积 -尝试使用 `cargo run` 运行程序: +现在使用 `cargo run` 运行程序: ```text The area of the rectangle is 1500 square pixels. @@ -36,21 +36,21 @@ The area of the rectangle is 1500 square pixels. ### 使用元组重构 -虽然示例 5-8 可以运行,并调用 `area` 函数用长方形的每个维度来计算出面积,不过我们可以做的更好。长度和宽度是相关联的,因为他们在一起才能定义一个长方形。 +虽然示例 5-8 可以运行,并调用 `area` 函数用长方形的每个维度来计算出面积,不过我们可以做的更好。宽度和高度是相关联的,因为他们在一起才能定义一个长方形。 -这个做法的问题突显在 `area` 的签名上: +这些代码的问题突显在 `area` 的签名上: ```rust,ignore -fn area(length: u32, width: u32) -> u32 { +fn area(width: u32, height: u32) -> u32 { ``` -函数 `area` 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。第三章的 “将值组合进元组” 部分已经讨论过了一种可行的方法:元组。示例 5-9 是另一个使用元组的版本: +函数 `area` 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。第三章的 “将值组合进元组” 部分已经讨论过了一种可行的方法:元组。示例 5-9 是另一个使用元组的版本: 文件名: src/main.rs ```rust fn main() { - let rect1 = (50, 30); + let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", @@ -63,26 +63,26 @@ fn area(dimensions: (u32, u32)) -> u32 { } ``` -示例 5-8:使用元组来指定长方形的长宽 +示例 5-8:使用元组来指定长方形的宽高 -在某种程度上说这样好一点了。元组帮助我们增加了一些结构性,现在在调用 `area` 的时候只用传递一个参数。不过在另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: +在某种程度上说这个程序更好一点了。元组帮助我们增加了一些结构性,现在在调用 `area` 的时候只需传递一个参数。不过在另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: -在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引 `0` 是 `length` 而 `1` 是 `width`。如果其他人要使用这些代码,他们也不得不搞清楚并记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义。 +在面积计算时混淆宽高并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引 `0` 是 `length` 而 `1` 是 `width`。如果其他人要使用这些代码,他们也不得不搞清楚并记住他们。容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义。 -### 使用结构体重构:增加更多意义 +### 使用结构体重构:赋予更多意义 -现在是引入结构体的时候了。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如示例 5-10 所示: +我们使用结构体为数据命令来为其赋予意义。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如示例 5-10 所示: 文件名: src/main.rs ```rust struct Rectangle { - length: u32, width: u32, + height: u32, } fn main() { - let rect1 = Rectangle { length: 50, width: 30 }; + let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", @@ -91,32 +91,32 @@ fn main() { } fn area(rectangle: &Rectangle) -> u32 { - rectangle.length * rectangle.width + rectangle.width * rectangle.height } ``` 示例 5-10:定义 `Rectangle` 结构体 -这里我们定义了一个结构体并称其为 `Rectangle`。在 `{}` 中定义了字段 `length` 和 `width`,都是 `u32` 类型的。接着在 `main` 中,我们创建了一个长度为 50 和宽度为 30 的 `Rectangle` 的具体实例。 +这里我们定义了一个结构体并称其为 `Rectangle`。在 `{}` 中定义了字段 `length` 和 `width`,都是 `u32` 类型的。接着在 `main` 中,我们创建了一个宽度为 30 和高度为 50 的 `Rectangle` 的具体实例。 -函数 `area` 现在被定义为接收一个名叫 `rectangle` 的参数,它的类型是一个结构体 `Rectangle` 实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样 `main` 函数就可以保持 `rect1` 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 `&`。 +函数 `area` 现在被定义为接收一个名叫 `rectangle` 的参数,其类型是一个结构体 `Rectangle` 实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样 `main` 函数就可以保持 `rect1` 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 `&`。 -`area` 函数访问 `Rectangle` 的 `length` 和 `width` 字段。`area` 的签名现在明确的表明了我们的意图:通过其 `length` 和 `width` 字段,计算一个 `Rectangle` 的面积。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 `0` 和 `1` 。结构体胜在更清晰明了。 +`area` 函数访问 `Rectangle` 的 `length` 和 `width` 字段。`area` 的签名现在明确的表明了我们的意图:通过其 `length` 和 `width` 字段,计算一个 `Rectangle` 的面积。这表明了宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 `0` 和 `1` 。结构体胜在更清晰明了。 -### 通过衍生 trait 增加实用功能 +### 通过派生 trait 增加实用功能 -如果能够在调试程序时打印出 `Rectangle` 实例来查看其所有字段的值就更好了。示例 5-11 像往常一样使用 `println!` 宏: +如果能够在调试程序时打印出 `Rectangle` 实例来查看其所有字段的值就更好了。示例 5-11 像第二章、第三章和第四章那样尝试了 `println!` 宏: 文件名: src/main.rs ```rust,ignore struct Rectangle { - length: u32, width: u32, + height: u32, } fn main() { - let rect1 = Rectangle { length: 50, width: 30 }; + let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {}", rect1); } @@ -130,12 +130,12 @@ fn main() { error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied ``` -`println!` 宏能处理很多类型的格式,不过,`{}`,默认告诉 `println!` 使用被称为 `Display` 的格式:直接提供给终端用户查看的输出。目前为止见过的基本类型都默认实现了 `Display`,因为它就是向用户展示 `1` 或其他任何基本类型的唯一方式。不过对于结构体,`println!` 应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出结构体的 `{}` 吗?所有字段都应该显示吗?因为这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个 `Display` 实现。 +`println!` 宏能处理很多类型的格式,不过,`{}` 默认告诉 `println!` 使用被称为 `Display` 的格式:意在提供给直接终端用户查看的输出。目前为止见过的基本类型都默认实现了 `Display`,因为它就是向用户展示 `1` 或其他任何基本类型的唯一方式。不过对于结构体,`println!` 应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?由于这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个 `Display` 实现。 但是如果我们继续阅读错误,将会发现这个有帮助的信息: ```text -note: `Rectangle` cannot be formatted with the default formatter; try using +`Rectangle` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string ``` @@ -144,13 +144,13 @@ note: `Rectangle` cannot be formatted with the default formatter; try using 让我们试试运行这个变化。见鬼了!仍然能看到一个错误: ```text -error: the trait bound `Rectangle: std::fmt::Debug` is not satisfied +error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied ``` 不过编译器又一次给出了一个有帮助的信息! ```text -note: `Rectangle` cannot be formatted using `:?`; if it is defined in your +`Rectangle` cannot be formatted using `:?`; if it is defined in your crate, add `#[derive(Debug)]` or manually implement it ``` @@ -161,34 +161,34 @@ Rust **确实** 包含了打印出调试信息的功能,不过我们必须为 ```rust #[derive(Debug)] struct Rectangle { - length: u32, width: u32, + height: u32, } fn main() { - let rect1 = Rectangle { length: 50, width: 30 }; + let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:?}", rect1); } ``` -示例 5-12:增加注解来导出 `Debug` trait +示例 5-12:增加注解来派生 `Debug` trait,并使用调试格式打印 `Rectangle` 实例 -现在我们再运行这个程序时,不会有任何错误并会出现如下输出: +现在我们再运行这个程序时,就不会有任何错误并会出现如下输出了: ```text -rect1 is Rectangle { length: 50, width: 30 } +rect1 is Rectangle { width: 30, height: 50 } ``` -好极了!这并不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。如果想要输出再好看和易读一点,可以将 `println!` 的字符串中的 `{:?}` 替换为 `{:#?} `,这对更大的结构体会有帮助。如果在这个例子中使用了 `{:#?}` 风格的话,输出会看起来像这样: +好极了!这并不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。当我们有一个更大的结构体时,能有更易读一点的输出就好了,为此可以使用 `{:#?}` 替换 `println!` 字符串中的 `{:?}`。如果在这个例子中使用了 `{:#?}` 风格的话,输出会看起来像这样: ```text rect1 is Rectangle { - length: 50, - width: 30 + width: 30, + height: 50 } ``` Rust 为我们提供了很多可以通过 `derive` 注解来使用的 trait,他们可以为我们的自定义类型增加实用的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。 -我们的 `area` 函数是非常特化的,它只是计算了长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的`area` **方法** 中。 +我们的 `area` 函数是非常特化的,它只是计算了长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的 `area` **方法** 中。 diff --git a/src/ch05-03-method-syntax.md b/src/ch05-03-method-syntax.md index 116f5fe..a8d5d08 100644 --- a/src/ch05-03-method-syntax.md +++ b/src/ch05-03-method-syntax.md @@ -2,9 +2,9 @@ > [ch05-03-method-syntax.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch05-03-method-syntax.md) >
-> commit 44bf3afd93519f8b0f900f21a5f2344d36e13448 +> commit ec65990849230388e4ce4db5b7a0cb8a0f0d60e2 -**方法** 与函数类似:它们使用 `fn` 关键字和名字声明,可以拥有参数和返回值,同时包含一段该方法在某处被调用时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章和第十七章讲解),并且它们第一个参数总是 `self`,它代表调用该方法的结构体实例。 +**方法** 与函数类似:它们使用 `fn` 关键字和名称声明,可以拥有参数和返回值,同时包含一段该方法在某处被调用时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第六章和第十七章讲解),并且它们第一个参数总是 `self`,它代表调用该方法的结构体实例。 ### 定义方法 @@ -15,18 +15,18 @@ ```rust #[derive(Debug)] struct Rectangle { - length: u32, width: u32, + height: u32, } impl Rectangle { fn area(&self) -> u32 { - self.length * self.width + self.width * self.height } } fn main() { - let rect1 = Rectangle { length: 50, width: 30 }; + let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", @@ -41,9 +41,9 @@ fn main() { 在 `area` 的签名中,开始使用 `&self` 来替代 `rectangle: &Rectangle`,因为该方法位于 `impl Rectangle` 上下文中所以 Rust 知道 `self` 的类型是 `Rectangle`。注意仍然需要在 `self` 前面加上 `&`,就像 `&Rectangle` 一样。方法可以选择获取 `self` 的所有权,或者像我们这里一样不可变地借用 `self`,或者可变地借用 `self`,就跟其他别的参数一样。 -这里选择 `&self` 跟在函数版本中使用 `&Rectangle` 出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 `&mut self`。很少见到通过仅仅使用 `self` 作为第一个参数来使方法获取实例的所有权;这种技术通常用在当方法将 `self` 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。 +这里选择 `&self` 跟在函数版本中使用 `&Rectangle` 出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 `&mut self`。通过仅仅使用 `self` 作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 `self` 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。 -尽量使用方法替代函数,除了使用了方法语法和不需要在每个函数签名中重复 `self` 类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 `impl` 块中,而不是让将来的用户在我们的代码中到处寻找 `Rectangle` 的功能。 +使用方法替代函数,除了使用了方法语法和不需要在每个函数签名中重复 `self` 类型之外,其主要好处在于组织性。我们将某个类型实例能做的所有事情都一起放入 `impl` 块中,而不是让将来的用户在我们的库中到处寻找 `Rectangle` 的功能。 > ### `->`运算符到哪去了? > @@ -51,7 +51,7 @@ fn main() { > > Rust 并没有一个与 `->` 等效的运算符;相反,Rust 有一个叫 **自动引用和解引用**(*automatic referencing and dereferencing*)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。 > -> 这是它如何工作的:当使用 `object.something()` 调用方法时,Rust 会自动增加 `&`、`&mut` 或 `*` 以便使 `object` 符合方法的签名。也就是说,这些代码是等同的: +> 这是它如何工作的:当使用 `object.something()` 调用方法时,Rust 会自动增加 `&`、`&mut` 或 `*` 以便使 `object` 符合方法的签名。也就是说,这些代码是等价的: > > ```rust > # #[derive(Debug,Copy,Clone)] @@ -74,7 +74,7 @@ fn main() { > (&p1).distance(&p2); > ``` > -> 第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者————`self` 类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(`&self`),做出修改(`&mut self`)或者是获取所有权(`self`)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。 +> 第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者———— `self` 类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(`&self`),做出修改(`&mut self`)或者是获取所有权(`self`)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。 ### 带有更多参数的方法 @@ -84,9 +84,9 @@ fn main() { ```rust,ignore fn main() { - let rect1 = Rectangle { length: 50, width: 30 }; - let rect2 = Rectangle { length: 40, width: 10 }; - let rect3 = Rectangle { length: 45, width: 60 }; + 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)); @@ -95,31 +95,31 @@ fn main() { 示例 5-14:展示还未实现的 `can_hold` 方法的应用 -我们希望看到如下输出,因为 `rect2` 的长宽都小于 `rect1`,而 `rect3` 比 `rect1` 要宽: +同时我们希望看到如下输出,因为 `rect2` 的宽高都小于 `rect1`,而 `rect3` 比 `rect1` 要宽: ```text Can rect1 hold rect2? true Can rect1 hold rect3? false ``` -因为我们想定义一个方法,所以它应该位于 `impl Rectangle` 块中。方法名是 `can_hold`,并且它会获取另一个 `Rectangle` 的不可变借用作为参数。通过观察调用位置的代码可以看出参数是什么类型的:`rect1.can_hold(&rect2)` 传入了 `&rect2`,它是一个 `Rectangle` 的实例 `rect2` 的不可变借用。这是可以理解的,因为我们只需要读取 `rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望 `main` 保持 `rect2` 的所有权这样就可以在调用这个方法后继续使用它。`can_hold` 的返回值是一个布尔值,其实现会分别检查 `self` 的长宽是否都大于另一个 `Rectangle`。让我们在示例 5-13 的 `impl` 块中增加这个新方法,如示例 5-15 所示: +因为我们想定义一个方法,所以它应该位于 `impl Rectangle` 块中。方法名是 `can_hold`,并且它会获取另一个 `Rectangle` 的不可变借用作为参数。通过观察调用位置的代码可以看出参数是什么类型的:`rect1.can_hold(&rect2)` 传入了 `&rect2`,它是一个 `Rectangle` 的实例 `rect2` 的不可变借用。这是可以理解的,因为我们只需要读取 `rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望 `main` 保持 `rect2` 的所有权这样就可以在调用这个方法后继续使用它。`can_hold` 的返回值是一个布尔值,其实现会分别检查 `self` 的宽高是否都大于另一个 `Rectangle`。让我们在示例 5-13 的 `impl` 块中增加这个新方法,如示例 5-15 所示: 文件名: src/main.rs ```rust # #[derive(Debug)] # struct Rectangle { -# length: u32, # width: u32, +# height: u32, # } # impl Rectangle { fn area(&self) -> u32 { - self.length * self.width + self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { - self.length > other.length && self.width > other.width + self.width > other.width && self.height > other.height } } ``` @@ -130,22 +130,22 @@ impl Rectangle { ### 关联函数 -`impl` 块的另一个有用的功能是:允许在 `impl` 块中定义 **不** 以 `self` 作为参数的函数。这被称为 **关联函数**(*associated functions*),因为它们与结构体相关联。即便如此它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过一个关联函数了:`String::from`。 +`impl` 块的另一个有用的功能是:允许在 `impl` 块中定义 **不** 以 `self` 作为参数的函数。这被称为 **关联函数**(*associated functions*),因为它们与结构体相关联。即便如此它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。我们已经使用过 `String::from` 关联函数了。 -关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时用来作为长和宽,这样可以更轻松的创建一个正方形 `Rectangle` 而不必指定两次同样的值: +关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时用来作为宽和高,这样可以更轻松的创建一个正方形 `Rectangle` 而不必指定两次同样的值: -Filename: src/main.rs +文件名: src/main.rs ```rust # #[derive(Debug)] # struct Rectangle { -# length: u32, # width: u32, +# height: u32, # } # impl Rectangle { fn square(size: u32) -> Rectangle { - Rectangle { length: size, width: size } + Rectangle { width: size, height: size } } } ``` @@ -159,25 +159,27 @@ impl Rectangle { ```rust # #[derive(Debug)] # struct Rectangle { -# length: u32, # width: u32, +# height: u32, # } # impl Rectangle { fn area(&self) -> u32 { - self.length * self.width + self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { - self.length > other.length && self.width > other.width + self.width > other.width && self.height > other.height } } ``` 示例 5-16:使用多个 `impl` 块重写示例 5-15 +没有理由将这些方法分散在多个 `impl` 块中,不过这是有效的语法。第十章讨论泛型和 trait 时会看到实用的多 `impl` 块的用例。 + ## 总结 结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。 diff --git a/src/ch06-00-enums.md b/src/ch06-00-enums.md index 9b34e78..ebf4b20 100644 --- a/src/ch06-00-enums.md +++ b/src/ch06-00-enums.md @@ -6,4 +6,4 @@ 本章介绍 **枚举**(*enumerations*),也被称作 *enums*。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 `Option`,它代表一个值要么是某个值要么什么都不是。然后会讲到在 `match` 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会涉及到 `if let`,另一个简洁方便处理代码中枚举的结构。 -枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与 F#、OCaml 和 Haskell 这样的函数式编程语言中的 **代数数据类型**(*algebraic data types*)最为相似。 +枚举是一个很多语言都有的功能,不过不同语言中其功能各不相同。Rust 的枚举与 F#、OCaml 和 Haskell 这样的函数式编程语言中的 **代数数据类型**(*algebraic data types*)最为相似。 diff --git a/src/ch06-01-defining-an-enum.md b/src/ch06-01-defining-an-enum.md index 182f2df..fa47618 100644 --- a/src/ch06-01-defining-an-enum.md +++ b/src/ch06-01-defining-an-enum.md @@ -2,11 +2,11 @@ > [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch06-01-defining-an-enum.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 5544b998ff426aca7d1eaf248a1d9340df5ab9e7 -让我们在一个实际场景中看看在这个特定场景下,使用枚举比使用结构体更合适。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 **枚举** 出所有可能的值,这也正是它名字的由来。 +让我们看看一个需要诉诸于代码的场景,来考虑为何此时使用枚举更为合适且实用。假设我们要处理 IP 地址。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序可能会遇到的所有可能的 IP 地址类型:所以可以 **枚举** 出所有可能的值,这也正是此枚举名字的由来。 -任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的,而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把它们当作相同的类型。 +任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的,而且不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值只可能是其中一个成员。IPv4 和 IPv6 从根本上讲仍是 IP 地址,所以当代码在处理适用于任何类型的 IP 地址的场景时应该把它们当作相同的类型。 可以通过在代码中定义一个 `IpAddrKind` 枚举来表现这个概念并列出可能的 IP 地址类型,`V4` 和 `V6`。这被称为枚举的 **成员**(*variants*): @@ -44,7 +44,7 @@ let six = IpAddrKind::V6; fn route(ip_type: IpAddrKind) { } ``` -现在可以使用任意成员来调用这个函数: +现在可以使用任一成员来调用这个函数: ```rust # enum IpAddrKind { @@ -58,7 +58,7 @@ route(IpAddrKind::V4); route(IpAddrKind::V6); ``` -使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址 **数据** 的方法;只知道它是什么 **类型** 的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样修改这个问题: +使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址 **数据** 的方法;只知道它是什么 **类型** 的。考虑到已经在第五章学习过结构体了,你可能会像示例 6-1 那样处理这个问题: ```rust enum IpAddrKind { @@ -101,7 +101,7 @@ let loopback = IpAddr::V6(String::from("::1")); 我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。 -用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址储存为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String`,这就不能使用结构体了。枚举可以轻易处理的这个情况: +用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 `V4` 地址储存为四个 `u8` 值而 `V6` 地址仍然表现为一个 `String`,这就不能使用结构体了。枚举则可以轻易处理的这个情况: ```rust enum IpAddr { @@ -114,7 +114,7 @@ let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); ``` -这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了[以致标准库提供了一个可供使用的定义!][IpAddr]让我们看看标准库如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的成员的定义是不同的: +这些代码展示了使用枚举来储存两种不同 IP 地址的几种可能的选择。然而,事实证明储存和编码 IP 地址实在是太常见了[以致标准库提供了一个开箱即用的定义!][IpAddr]让我们看看标准库是如何定义 `IpAddr` 的:它正有着跟我们定义和使用的一样的枚举和成员,不过它将成员中的地址数据嵌入到了两个不同形式的结构体中,它们对不同的成员的定义是不同的: [IpAddr]: https://doc.rust-lang.org/std/net/enum.IpAddr.html @@ -157,7 +157,7 @@ enum Message { * `Write` 包含单独一个 `String`。 * `ChangeColor` 包含三个 `i32`。 -定义一个像示例 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用 `struct` 关键字而且所有成员都被组合在一起位于 `Message` 下之外。如下这些结构体可以包含与之前枚举成员中相同的数据: +定义一个像示例 6-2 中的枚举类似于定义不同类型的结构体,除了枚举不使用 `struct` 关键字并且所有成员都被组合在一起位于 `Message` 下之外。如下这些结构体可以包含与之前枚举成员中相同的数据: ```rust struct QuitMessage; // unit struct @@ -212,7 +212,7 @@ m.call(); > crashes, which have probably caused a billion dollars of pain and damage in > the last forty years. > -> 我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抗拒引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。 +> 我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。 空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性是无处不在的,非常容易出现这类错误。 @@ -229,7 +229,7 @@ enum Option { } ``` -`Option` 是如此有用以至于它甚至被包含在了 prelude 之中,这意味着我们不需要显式导入它。另外,它的成员也是如此,可以不需要 `Option::` 前缀来直接使用 `Some` 和 `None`。即便如此 `Option` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option` 的成员。 +`Option` 是如此有用以至于它甚至被包含在了 prelude 之中,这意味着我们不需要显式引入作用域。另外,它的成员也是如此,可以不需要 `Option::` 前缀来直接使用 `Some` 和 `None`。即便如此 `Option` 也仍是常规的枚举,`Some(T)` 和 `None` 仍是 `Option` 的成员。 `` 语法是一个我们还未讲到的 Rust 功能。它是一个泛型类型参数,第十章会更详细的讲解泛型。目前,所有你需要知道的就是 `` 意味着 `Option` 枚举的 `Some` 成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型 `Option` 值的例子: @@ -261,14 +261,14 @@ error[E0277]: the trait bound `i8: std::ops::Add>` is not satisfied --> | -7 | let sum = x + y; - | ^^^^^ +5 | let sum = x + y; + | ^ no implementation for `i8 + std::option::Option` | ``` 哇哦!事实上,错误信息意味着 Rust 不知道该如何将 `Option` 与 `i8` 相加。当在 Rust 中拥有一个像 `i8` 这样类型的值时,编译器确保它总是有一个有效的值。我们可以自信使用而无需判空。只有当使用 `Option`(或者任何用到的类型)的时候需要担心可能没有一个值,而编译器会确保我们在使用值之前处理为空的情况。 -换句话说,在对 `Option` 进行 `T` 的运算之前必须转换为 `T`。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空的情况。 +换句话说,在对 `Option` 进行 `T` 的运算之前必须将其转换为 `T`。通常这能帮助我们捕获空值最常见的问题之一:假设某值不为空但实际上为空的情况。 无需担心错过存在非空值的假设让我们对代码更加有信心,为了拥有一个可能为空的值,必须显式的将其放入对应类型的 `Option` 中。接着,当使用这个值时,必须明确的处理值为空的情况。任何地方一个值不是 `Option` 类型的话,**可以** 安全的假设它的值不为空。这是 Rust 的一个有意为之的设计选择,来限制空值的泛滥和增加 Rust 代码的安全性。 @@ -276,4 +276,4 @@ not satisfied [docs]: https://doc.rust-lang.org/std/option/enum.Option.html -总的来说,为了使用 `Option` 值,需要编写处理每个成员的代码。我们想要一些代码只当拥有 `Some(T)` 值时运行,这些代码允许使用其中的 `T`。也希望一些代码当在 `None` 值时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。 +总的来说,为了使用 `Option` 值,需要编写处理每个成员的代码。我们想要一些代码只当拥有 `Some(T)` 值时运行,这些代码允许使用其中的 `T`。也希望一些代码在 `None` 值时运行,这些代码并没有一个可用的 `T` 值。`match` 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。