Merge pull request #8 from KaiserY/master

Update from KaiserY
pull/288/head
Tengfei Niu 7 years ago committed by GitHub
commit a4a52cbe15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,20 +1,20 @@
## Slices
## Slice 类型
> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-03-slices.md)
> <br>
> commit 88a12e16d4c7fa669349c9b1ddb48093de92c5e6
> commit 729f1118332ddcf01138d284eac5db0e85f8c8ed
另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
这里有一个编程小题:编写一个函数,该函数接收一个字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则意味着整个字符串是一个单词,所以应该返回整个字符串。
这里有一个编程小题:编写一个函数,该函数接收一个字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串是一个单词,所以应该返回整个字符串。
让我们考虑一下这个函数的声明
让我们考虑一下这个函数的签名
```rust,ignore
fn first_word(s: &String) -> ?
```
`first_word` 函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。试试如示例 4-5 所示的代码:
`first_word` 函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。试试如示例 4-7 中的代码。
<span class="filename">文件名: src/main.rs</span>
@ -32,9 +32,9 @@ fn first_word(s: &String) -> usize {
}
```
<span class="caption">示例 4-5`first_word` 函数返回 `String` 参数的一个字节索引值</span>
<span class="caption">示例 4-7`first_word` 函数返回 `String` 参数的一个字节索引值</span>
让我们将代码分解成小块。因为需要一个元素一个元素的检查 `String` 中的值是否为空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组:
因为需要逐个元素的检查 `String` 中的值是否为空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组:
```rust,ignore
let bytes = s.as_bytes();
@ -46,21 +46,22 @@ let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
```
第十三章将会讨论迭代器的更多细节。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装 `iter` 的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。
我们将在第十三章详细讨论迭代器。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装 `iter` 的结果并返回一个元组,其中每一个元素是元组的一部分。`enumerate` 返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。
因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构,就像 Rust 中其他任何地方所做的一样。所以在 `for` 循环中,我们指定了一个模式,其中 `i` 是元组中的索引而 `&item` 是单个字节。因为我们从 `.iter().enumerate()` 中获取了集合元素的引用,所以模式中使用了 `&`
因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构,就像 Rust 中其他任何地方所做的一样。所以在 `for` 循环中,我们指定了一个模式,其中元组中的 `i`索引而元组中的 `&item` 是单个字节。因为我们从 `.iter().enumerate()` 中获取了集合元素的引用,所以模式中使用了 `&`
我们通过字节的字面值语法来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用 `s.len()` 返回字符串的长度:
`for` 循环中,我们通过字节的字面值语法来寻找代表空格的字节。如果找到了一个空格,返回它的位置。否则,使用 `s.len()` 返回字符串的长度:
```rust,ignore
if item == b' ' {
return i;
}
}
s.len()
```
现在有了一个找到字符串中第一个单词结尾索引的方法,不过这有一个问题。我们返回了单独一个 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下示例 4-6 中使用了示例 4-5 中 `first_word` 函数的程序:
现在有了一个找到字符串中第一个单词结尾索引的方法,不过这有一个问题。我们返回了一个独立的 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下示例 4-8 中使用了示例 4-7 中 `first_word` 函数的程序。
<span class="filename">文件名: src/main.rs</span>
@ -80,28 +81,28 @@ s.len()
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5.
let word = first_word(&s); // word 的值为 5
s.clear(); // This empties the String, making it equal to "".
s.clear(); // 这清空了字符串,使其等于 ""
// word still has the value 5 here, but there's no more string that
// we could meaningfully use the value 5 with. word is now totally invalid!
// word 在此处的值仍然是 5
// 但是没有更多的字符串让我们可以有效地应用数值 5。word 的值现在完全无效!
}
```
<span class="caption">示例 4-6储存 `first_word` 函数调用的返回值并接着改变 `String` 的内容</span>
<span class="caption">示例 4-8存储 `first_word` 函数调用的返回值并接着改变 `String` 的内容</span>
这个程序编译时没有任何错误,而且在调用 `s.clear()` 之后使用 `word` 也不会出错。这时 `word``s` 状态就完全没有联系了,所以 `word `仍然包含值 `5`。可以尝试用值 `5` 来提取变量 `s` 的第一个单词,不过这是有 bug 的,因为在我们将 `5` 保存到 `word` 之后 `s` 的内容已经改变。
这个程序编译时没有任何错误,而且在调用 `s.clear()` 之后使用 `word` 也不会出错。因为 `word``s` 状态完全没有联系,所以 `word ` 仍然包含值 `5`。可以尝试用值 `5` 来提取变量 `s` 的第一个单词,不过这是有 bug 的,因为在我们将 `5` 保存到 `word` 之后 `s` 的内容已经改变。
我们不得不时刻担心 `word` 的索引与 `s` 中的数据不再同步,这是冗余且容易出错的!如果编写这么一个 `second_word` 函数的话,管理索引这件事将更加容易出问题。它的声明看起来像这样:
我们不得不时刻担心 `word` 的索引与 `s` 中的数据不再同步,这很啰嗦且易出错!如果编写这么一个 `second_word` 函数的话,管理索引这件事将更加容易出问题。它的签名看起来像这样:
```rust,ignore
fn second_word(s: &String) -> (usize, usize) {
```
现在我们跟踪一个开始索引 **和** 一个结尾索引,同时有了更多从数据的某个特定状态计算而来的值,他们也完全没有与这个状态相关联。现在有了三个飘忽不定的不相关变量都需要被同步。
现在我们跟踪一个开始索引 **和** 一个结尾索引,同时有了更多从数据的某个特定状态计算而来的值,但都完全没有与这个状态相关联。现在有三个飘忽不定的不相关变量需要保持同步。
幸运的是Rust 为这个问题提供了一个解决方:字符串 slice。
幸运的是Rust 为这个问题提供了一个解决方:字符串 slice。
### 字符串 slice
@ -114,9 +115,9 @@ let hello = &s[0..5];
let world = &s[6..11];
```
这类似于获取整个 `String` 的引用不过带有额外的 `[0..5]` 部分。不同于整个 `String` 的引用,这是一个包含 `String` 内部的一个位置和所需元素数量的引用。`start..end` 语法代表一个以 `start` 开头并一直持续到但不包含 `end` 的 range。
这类似于引用整个 `String` 不过带有额外的 `[0..5]` 部分。不是对整个 `String` 的引用,而是对部分 `String` 的引用。`start..end` 语法代表一个以 `start` 开头并一直持续到但不包含 `end` 的 range。
使用一个由中括号中的 `[starting_index..ending_index]` 指定的 range 创建一个 slice其中 `starting_index`包含在 slice 的第一个位置,`ending_index` 则是 slice 最后一个位置的后一个值。在其内部slice 的数据结构存了开始位置和 slice 的长度,长度对应 `ending_index` 减去 `starting_index` 的值。所以对于 `let world = &s[6..11];` 的情况,`world` 将是一个包含指向 `s`6 个字节的指针和长度值 5 的 slice。
使用一个由中括号中的 `[starting_index..ending_index]` 指定的 range 创建一个 slice其中 `starting_index` 是 slice 的第一个位置,`ending_index` 则是 slice 最后一个位置的后一个值。在其内部slice 的数据结构存了 slice 的开始位置和长度,长度对应 `ending_index` 减去 `starting_index` 的值。所以对于 `let world = &s[6..11];` 的情况,`world` 将是一个包含指向 `s`7 个字节的指针和长度值 5 的 slice。
图 4-6 展示了一个图例。
@ -144,7 +145,7 @@ let slice = &s[3..len];
let slice = &s[3..];
```
也可以同时舍弃这两个值来获取一个整个字符串的 slice。所以如下亦是相同的
也可以同时舍弃这两个值来获取整个字符串的 slice。所以如下亦是相同的
```rust
let s = String::from("hello");
@ -155,7 +156,7 @@ let slice = &s[0..len];
let slice = &s[..];
```
> 注意:字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice则程序将会因错误而退出。出于介绍字符串 slice 的目的,本部分假设只使用 ASCII 字符集;第八章的 “字符串” 部分会更加全面的讨论 UTF-8 处理问题。
> 注意:字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice则程序将会因错误而退出。出于介绍字符串 slice 的目的,本部分假设只使用 ASCII 字符集;第八章的 “使用字符串存储 UTF-8 编码的文本” 部分会更加全面的讨论 UTF-8 处理问题。
在记住所有这些知识后,让我们重写 `first_word` 来返回一个 slice。“字符串 slice” 的类型声明写作 `&str`
@ -175,9 +176,9 @@ fn first_word(s: &String) -> &str {
}
```
我们使用跟示例 4-5 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。
我们使用跟示例 4-7 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当找到一个空格,我们返回一个字符串 slice它使用字符串的开始和空格的索引作为开始和结束的索引。
现在当调用 `first_word` 时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。
现在当调用 `first_word` 时,会返回与底层数据关联的单个值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。
`second_word` 函数也可以改为返回一个 slice
@ -185,7 +186,7 @@ fn first_word(s: &String) -> &str {
fn second_word(s: &String) -> &str {
```
现在我们有了一个不易混淆且直观的 API 了,因为编译器会确保指向 `String` 的引用持续有效。还记得示例 4-6 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上是不正确的,不过却没有表现出任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误:
现在我们有了一个不易混淆且直观的 API 了,因为编译器会确保指向 `String` 的引用持续有效。还记得示例 4-8 程序中,那个当我们获取第一个单词结尾的索引后,接着就清除了字符串导致索引就无效的 bug 吗?那些代码在逻辑上是不正确的,但却没有显示任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误:
<span class="filename">文件名: src/main.rs</span>
@ -195,7 +196,7 @@ fn main() {
let word = first_word(&s);
s.clear(); // Error!
s.clear(); // error!
}
```
@ -208,7 +209,7 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
4 | let word = first_word(&s);
| - immutable borrow occurs here
5 |
6 | s.clear(); // Error!
6 | s.clear(); // error!
| ^ mutable borrow occurs here
7 | }
| - immutable borrow ends here
@ -228,19 +229,21 @@ let s = "Hello, world!";
#### 字符串 slice 作为参数
在知道了能够获取字面值和 `String` 的 slice 后引起了另一个对 `first_word` 的改进,这是它的声明
在知道了能够获取字面值和 `String` 的 slice 后,我们对 `first_word` 做了改进,这是它的签名
```rust,ignore
fn first_word(s: &String) -> &str {
```
相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对 `String``&str` 使用相同的函数:
而更有经验的 Rustacean 会编写出示例 4-9 中的签名,因为它使得可以对 `String` 值和 `&str`使用相同的函数:
```rust,ignore
fn first_word(s: &str) -> &str {
```
如果有一个字符串 slice可以直接传递它。如果有一个 `String`,则可以传递整个 `String` 的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能:
<span class="caption">示例 4-9: 通过将 `s` 参数的类型改为字符串 slice 来改进 `first_word` 函数</span>
如果有一个字符串 slice可以直接传递它。如果有一个 `String`,则可以传递整个 `String` 的 slice。定义一个获取字符串 slice 而不是 `String` 引用的函数使得我们的 API 更加通用并且不会丢失任何功能:
<span class="filename">Filename: src/main.rs</span>
@ -259,16 +262,16 @@ fn first_word(s: &str) -> &str {
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[..]);
// since string literals *are* string slices already,
// this works too, without the slice syntax!
// 因为字符串字面值 **就是** 字符串 slice
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}
```
@ -281,7 +284,7 @@ fn main() {
let a = [1, 2, 3, 4, 5];
```
就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分,而我们可以这样做:
就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分我们可以这样做:
```rust
let a = [1, 2, 3, 4, 5];
@ -289,10 +292,10 @@ let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
```
这个 slice 的类型是 `&[i32]`。它跟字符串 slice 一样方式工作,通过存第一个集合元素的引用和一个集合总长度。你可以对其他所有类型的集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。
这个 slice 的类型是 `&[i32]`。它跟字符串 slice 的工作方式一样,通过存第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。
## 总结
所有权、借用和 slice 这些概念是 Rust 可以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个 `struct` 中。
所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始第五章,来看看如何将多份数据组合进一个 `struct` 中。

@ -4,9 +4,9 @@
> <br>
> commit c560db1e0145d5a64b9415c9cfe463c7dac31ab8
结构体和我们在第三章讨论过的元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字使得结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
结构体和我们在第三章讨论过的元组类似。和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。
定义结构体,需要使用 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,它们被称作 **字段***field*),并定义字段类型。例如,示例 5-1 展示了一个储存用户账号信息的结构体:
定义结构体,需要使用 `struct` 关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 **字段***field*)。例如,示例 5-1 展示了一个存储用户账号信息的结构体:
```rust
struct User {
@ -19,7 +19,7 @@ struct User {
<span class="caption">示例 5-1`User` 结构体定义</span>
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 键-值对的形式提供字段,其中 key 是字段的名字value 是需要存在字段中的数据值。实例中具体说明字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户:
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 **实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用 `key: value` 键-值对的形式提供字段,其中 key 是字段的名字value 是需要存在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。例如,可以像示例 5-2 这样来声明一个特定的用户:
```rust
# struct User {
@ -61,9 +61,9 @@ user1.email = String::from("anotheremail@example.com");
<span class="caption">示例 5-3改变 `User` 实例 `email` 字段的值</span>
注意整个实例必须是可变的Rust 并不允许只将特定字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
注意整个实例必须是可变的Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
示例 5-4 显示了一个返回带有给定的 `email``username``User` 结构体的实例的 `build_user` 函数。`active` 字段的值为 `true`,并且 `sign_in_count` 的值为 `1`
示例 5-4 显示了一个 `build_user` 函数,它返回一个带有给定的 email 和用户名的 `User` 结构体实例。`active` 字段的值为 `true`,并且 `sign_in_count` 的值为 `1`
```rust
# struct User {
@ -85,13 +85,12 @@ fn build_user(email: String, username: String) -> User {
<span class="caption">示例 5-4`build_user` 函数获取 email 和用户名并返回 `User` 实例</span>
为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 `email``username` 字段名称与变量有些冗余。如果结构体有更多字段,重复这些名称就显得更加烦人了。幸运的是,有一个方便的简写语法!
为函数参数起与结构体字段相同的名字是可以理解的,但是不得不重复 `email``username` 字段名称与变量有些啰嗦。如果结构体有更多字段,重复每个名称就更加烦人了。幸运的是,有一个方便的简写语法!
### 变量与字段同名时的字段初始化简写语法
因为示例 5-4 中的参数名与字段名都完全相同,我们可以使用 **字段初始化简写语法***field init shorthand*)来重写 `build_user`,这样其行为与之前完全相同,不过无需重复 `email``username` 了,如示例 5-5 所示。
```rust
# struct User {
# username: String,
@ -118,7 +117,7 @@ fn build_user(email: String, username: String) -> User {
使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有帮助的。这可以通过 **结构体更新语法***struct update syntax*)实现。
首先,示例 5-6 展示了如何不使用更新语法来`user2` 中创建一个新 `User` 实例。我们为 `email``username` 设置了新的值,其他值则使用了实例 5-2 中创建的 `user1` 中的同名值:
首先,示例 5-6 展示了不使用更新语法时,如何`user2` 中创建一个新 `User` 实例。我们为 `email``username` 设置了新的值,其他值则使用了实例 5-2 中创建的 `user1` 中的同名值:
```rust
# struct User {
@ -175,9 +174,9 @@ let user2 = User {
### 使用没有命名字段的元组结构体来创建不同的类型
也可以定义与元组(在第三章讨论过)类似的结构体,称为 **元组结构体***tuple structs*,有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。元组结构体在你希望命名整个元组并使其与其他(同样的)元组为不同类型时很有用,这时像常规结构体那样为每个字段命名就显得冗余和形式化了。
也可以定义与元组(在第三章讨论过)类似的结构体,称为 **元组结构体***tuple structs*。元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
定义元组结构体以 `struct` 关键字和结构体名开头并后跟元组中的类型。例如,这里是两个分别叫做 `Color``Point` 元组结构体的定义和用例
定义元组结构体`struct` 关键字和结构体名开头并后跟元组中的类型。例如,下面是两个分别叫做 `Color``Point` 元组结构体的定义和用法
```rust
struct Color(i32, i32, i32);
@ -187,17 +186,17 @@ let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
```
注意 `black``origin`是不同的类型,因为它们是不同的元组结构体的实例。我们定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。例如,一个获取 `Color` 类型参数的函数不能接受 `Point` 作为参数,即便这两个类型都由三个 `i32` 值组成。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 `.` 后跟索引来访问单独的值,等等。
注意 `black``origin` 值的类型不同,因为它们是不同的元组结构体的实例。定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。例如,一个获取 `Color` 类型参数的函数不能接受 `Point` 作为参数,即便这两个类型都由三个 `i32` 值组成。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 `.` 后跟索引来访问单独的值,等等。
### 没有任何字段的类单元结构体
我们也可以定义一个没有任何字段的结构体!它们被称为 **类单元结构体***unit-like structs*)因为它们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型存储数据的时候发挥作用。我们将在第十章介绍 trait。
我们也可以定义一个没有任何字段的结构体!它们被称为 **类单元结构体***unit-like structs*)因为它们类似于 `()`,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型存储数据的时候发挥作用。我们将在第十章介绍 trait。
> ## 结构体数据的所有权
>
> 在示例 5-1 中的 `User` 结构体的定义中,我们使用了自身拥有所有权的 `String` 类型而不是 `&str` 字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。
>
> 可以使结构体存被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期***lifetimes*),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存一个引用而不指定生命周期将是无效的,比如这样:
> 可以使结构体存被其他对象拥有的数据的引用,不过这么做的话需要用上 **生命周期***lifetimes*),这是一个第十章会讨论的 Rust 功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存一个引用而不指定生命周期将是无效的,比如这样:
>
> <span class="filename">文件名: src/main.rs</span>
>
@ -235,4 +234,4 @@ let origin = Point(0, 0, 0);
> | ^ expected lifetime parameter
> ```
>
> 第十章会讲到如何修复这个问题以便在结构体中存引用,不过现在,我们会使用像 `String` 这类拥有所有权的类型来替代 `&str` 这样的引用以修正这个错误。
> 第十章会讲到如何修复这个问题以便在结构体中存引用,不过现在,我们会使用像 `String` 这类拥有所有权的类型来替代 `&str` 这样的引用以修正这个错误。

@ -6,7 +6,7 @@
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。
使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的宽度和高度并计算它的面积。示例 5-8 中是项目的 *src/main.rs* 文件为此实现的一个小程序
使用 Cargo 新建一个叫做 *rectangles* 的二进制程序,它获取以像素为单位的长方形的宽度和高度,并计算出长方形的面积。示例 5-8 显示了位于项目的 *src/main.rs* 中的小程序,它刚刚好实现此功能
<span class="filename">文件名: src/main.rs</span>
@ -26,7 +26,7 @@ fn area(width: u32, height: u32) -> u32 {
}
```
<span class="caption">示例 5-8通过分别指定长方形的宽高变量来计算长方形面积</span>
<span class="caption">示例 5-8通过分别指定长方形的宽变量来计算长方形面积</span>
现在使用 `cargo run` 运行程序:
@ -34,7 +34,7 @@ fn area(width: u32, height: u32) -> u32 {
The area of the rectangle is 1500 square pixels.
```
虽然示例 5-8 可以运行,并调用 `area` 函数用长方形的每个维度来计算出面积,不过我们可以做的更好。宽度和高度是相关联的,因为他们在一起才能定义一个长方形。
虽然示例 5-8 可以运行,并在调用 `area` 函数时传入每个尺寸来计算出长方形的面积,不过我们可以做的更好。宽度和高度是相关联的,因为他们在一起才能定义一个长方形。
这些代码的问题突显在 `area` 的签名上:
@ -42,7 +42,7 @@ The area of the rectangle is 1500 square pixels.
fn area(width: u32, height: u32) -> u32 {
```
函数 `area` 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。第三章的 “元组类型” 部分已经讨论过了一种可行的方法:元组。
函数 `area` 本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序本身却没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。第三章的 “元组类型” 部分已经讨论过了一种可行的方法:元组。
### 使用元组重构
@ -67,13 +67,13 @@ fn area(dimensions: (u32, u32)) -> u32 {
<span class="caption">示例 5-9使用元组来指定长方形的宽高</span>
在某种程度上说这个程序更好一点了。元组帮助我们增加了一些结构性,并且现在只需传一个参数。不过在另一方面这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:
在某种程度上说这个程序更好一点了。元组帮助我们增加了一些结构性,并且现在只需传一个参数。不过在另一方面这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:
面积计算时混淆宽高并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引 `0``width``1``height`。如果其他人要使用这些代码,他们也不得不搞清楚并记住他们。这容易忘记或者混淆这些值而造成错误,因为我们没有表明代码中数据的意义
计算面积时将宽和高弄混倒无关紧要,不过当在屏幕上绘制长方形时就有问题了!我们必须牢记 `width` 的元组索引是 `0``height` 的元组索引是 `1`。如果其他人要使用这些代码,他们必须要搞清楚这一点,并也要牢记于心。很容易忘记或者混淆这些值而造成错误,因为我们没有在代码中传达数据的意图
### 使用结构体重构:赋予更多意义
我们使用结构体为数据命令来为其赋予意义。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如示例 5-10 所示:
我们使用结构体为数据命名来为其赋予意义。我们可以将我们正在使用的元组转换成一个有整体名称而且每个部分也有对应名字的数据类型,如示例 5-10 所示:
<span class="filename">文件名: src/main.rs</span>
@ -99,11 +99,11 @@ fn area(rectangle: &Rectangle) -> u32 {
<span class="caption">示例 5-10定义 `Rectangle` 结构体</span>
这里我们定义了一个结构体并称其为 `Rectangle`。在 `{}` 中定义了字段 `width``height`,都是 `u32` 类型的。接着在 `main` 中,我们创建了一个宽度为 30 和高度为 50 的 `Rectangle` 的具体实例
这里我们定义了一个结构体并称其为 `Rectangle`。在大括号中定义了字段 `width``height`类型都是 `u32`。接着在 `main` 中,我们创建了一个具体的 `Rectangle` 实例,它的宽是 30高是 50
函数 `area` 现在被定义为接收一个名叫 `rectangle` 的参数,其类型是一个结构体 `Rectangle` 实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权,这样 `main` 函数就可以保持 `rect1` 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 `&`
`area` 函数访问 `Rectangle``width``height` 字段。`area` 的签名现在明确的表明了我们的意图:通过其 `width``height` 字段,计算一个 `Rectangle` 的面积。这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 `0``1` 。结构体胜在更清晰明了。
`area` 函数访问 `Rectangle` 实例`width``height` 字段。`area` 的函数签名现在明确的阐述了我们的意图:使用 `Rectangle` `width``height` 字段,计算 `Rectangle` 的面积。这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 `0``1` 。结构体胜在更清晰明了。
### 通过派生 trait 增加实用功能
@ -126,13 +126,13 @@ fn main() {
<span class="caption">示例 5-11尝试打印出 `Rectangle` 实例</span>
如果运行代码,会出现带有如下核心信息的错误:
当我们运行这个代码时,会出现带有如下核心信息的错误:
```text
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` 实现。
但是如果我们继续阅读错误,将会发现这个有帮助的信息:
@ -141,9 +141,9 @@ error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
`:?` instead if you are using a format string
```
让我们来试试!现在 `println!` 宏调用看起来像 `println!("rect1 is {:?}", rect1);` 这样。在 `{}` 中加入 `:?` 指示符告诉 `println!` 我们想要使用叫做 `Debug` 的输出格式。`Debug` 是一个 trait它允许我们在调试代码时以一种对开发者有帮助的方式打印结构体。
让我们来试试!现在 `println!` 宏调用看起来像 `println!("rect1 is {:?}", rect1);` 这样。在 `{}` 中加入 `:?` 指示符告诉 `println!` 我们想要使用叫做 `Debug` 的输出格式。`Debug` 是一个 trait它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值
以这个改变运行程序。见鬼了!仍然能看到一个错误:
这样调整后再次运行程序。见鬼了!仍然能看到一个错误:
```text
error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
@ -176,7 +176,7 @@ fn main() {
<span class="caption">示例 5-12增加注解来派生 `Debug` trait并使用调试格式打印 `Rectangle` 实例</span>
现在我们再运行这个程序时,就不会有任何错误并会出现如下输出
现在我们再运行这个程序时,就不会有任何错误并会出现如下输出:
```text
rect1 is Rectangle { width: 30, height: 50 }
@ -191,6 +191,6 @@ rect1 is Rectangle {
}
```
Rust 为我们提供了很多可以通过 `derive` 注解来使用的 trait他们可以为我们的自定义类型增加实用的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait同时还有如何创建你自己的 trait。
Rust 为我们提供了很多可以通过 `derive` 注解来使用的 trait他们可以为我们的自定义类型增加实用的行为。附录 C 中列出了这些 trait 和行为。第十章会介绍如何通过自定义行为来实现这些 trait同时还有如何创建你自己的 trait。
我们的 `area` 函数是非常特化的,它只是计算了长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的 `area` **方法** 中。
我们的 `area` 函数是非常特殊的,它只计算长方形的面积。如果这个行为与 `Rectangle` 结构体再结合得更紧密一些就更好了,因为它不能用于其他类型。现在让我们看看如何继续重构这些代码,来将 `area` 函数协调进 `Rectangle` 类型定义的 `area` **方法** 中。

Loading…
Cancel
Save