diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md index c1d453d..4850b03 100644 --- a/src/ch04-02-references-and-borrowing.md +++ b/src/ch04-02-references-and-borrowing.md @@ -2,13 +2,13 @@ > [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit aa493fef8630e3eee865167892666569afbbc2aa -有这样一个问题:在上一部分的结尾处的使用元组的代码中,因为 `String` 先被移动到了 `calculate_length` 内,因此我们不得不将 `String` 作为返回值,这样在调用 `calculate_length` 后才仍然可以使用 `String`。 +上一部分结尾的元组代码有这样一个问题:我们不得不将 `String` 返回给调用函数,以便仍能在调用 `calculate_length` 后使用 `String`,因为 `String` 被移动到了 `calculate_length` 内。 下面是如何定义并使用一个(新的)`calculate_length` 函数,它以一个对象的 **引用** 作为参数而不是获取值的所有权: -Filename: src/main.rs +文件名: src/main.rs ```rust fn main() { @@ -26,12 +26,14 @@ fn calculate_length(s: &String) -> usize { 首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 `&s1` 给 `calculate_length`,同时在函数定义中,我们获取 `&String` 而不是 `String`。 -这些 & 符号就是 **引用**,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。 +这些 & 符号就是 **引用**,他们允许你使用值但不获取其所有权。图 4-5 展示了一个图解。 &String s pointing at String s1 图 4-8:`&String s` 指向 `String s1` +> 注意:与使用 `&` 引用相对的操作是 **解引用**(*dereferencing*),它使用解引用运算符,`*`。我们将会在第八章遇到一些解引用运算符,并在第十五章详细讨论解引用。 + 仔细看看这个函数调用: ```rust @@ -43,7 +45,7 @@ let s1 = String::from("hello"); let len = calculate_length(&s1); ``` -`&s1` 语法允许我们创建一个 **参考** 值 `s1` 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。 +`&s1` 语法允许我们创建一个 **指向** 值 `s1` 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。 同理,函数签名使用了 `&` 来表明参数 `s` 的类型是一个引用。让我们增加一些解释性的注解: @@ -54,11 +56,11 @@ fn calculate_length(s: &String) -> usize { // s is a reference to a String // it refers to, nothing happens. ``` -变量 `s` 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有所有权。 +变量 `s` 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有所有权。 我们将获取引用作为函数参数称为 **借用**(*borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。 -如果我们尝试修改借用的变量呢?尝试示例 4-9 中的代码。剧透:这行不通! +如果我们尝试修改借用的变量呢?尝试示例 4-4 中的代码。剧透:这行不通! 文件名: src/main.rs @@ -79,18 +81,20 @@ fn change(some_string: &String) { 这里是错误: ```text -error: cannot borrow immutable borrowed content `*some_string` as mutable +error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable --> error.rs:8:5 | +7 | fn change(some_string: &String) { + | ------- use `&mut String` here to make mutable 8 | some_string.push_str(", world"); - | ^^^^^^^^^^^ + | ^^^^^^^^^^^ cannot borrow as mutable ``` -正如变量默认是不可变的,引用也一样。不允许修改引用的值。 +正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。 ### 可变引用 -可以通过一个小调整来修复在示例 4-9 代码中的错误,在示例 4-9 的代码中: +可以通过一个小调整来修复在示例 4-4 代码中的错误: 文件名: src/main.rs @@ -106,7 +110,7 @@ fn change(some_string: &mut String) { } ``` -首先,必须将 `s` 改为 `mut`。然后必须创建一个可变引用 `&mut s` 和接受一个可变引用 `some_string: &mut String`。 +首先,必须将 `s` 改为 `mut`。然后必须创建一个可变引用 `&mut s` 和接受一个可变引用,`some_string: &mut String`。 不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败: @@ -133,7 +137,7 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time | - first borrow ends here ``` -这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候变量都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。 +这个限制允许可变性,不过是以一种受限制的方式允许。新 Rustacean 们经常与此作斗争,因为大部分语言中变量任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。 **数据竞争**(*data race*)是一种特定类型的竞争状态,它可由这三个行为造成: @@ -141,7 +145,7 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time 2. 至少有一个这样的指针被用来写入数据。 3. 不存在同步数据访问的机制。 -数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它直接拒绝编译存在数据竞争的代码! +数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码! 一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能 **同时** 拥有: @@ -186,13 +190,13 @@ immutable 尽管这些错误有时使人沮丧,但请牢记这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪,而不是任由你去追踪为何有时数据并不是你想象中的那样。 -### 悬垂引用 +### 悬垂引用(Dangling References) 在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个 **悬垂指针**(*dangling pointer*),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。 -让我们尝试创建一个悬垂引用: +让我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore fn main() { @@ -213,13 +217,11 @@ error[E0106]: missing lifetime specifier --> dangle.rs:5:16 | 5 | fn dangle() -> &String { - | ^^^^^^^ + | ^ expected lifetime parameter | - = help: this function's return type contains a borrowed value, but there is no - value for it to be borrowed from + = help: this function's return type contains a borrowed value, but there is + no value for it to be borrowed from = help: consider giving it a 'static lifetime - -error: aborting due to previous error ``` 错误信息引用了一个我们还未涉及到的功能:**生命周期**(*lifetimes*)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键: @@ -229,7 +231,7 @@ this function's return type contains a borrowed value, but there is no value for it to be borrowed from. ``` -让我们仔细看看我们的 `dangle` 代码的每一步到底放生了什么: +让我们仔细看看我们的 `dangle` 代码的每一步到底发生了什么: ```rust,ignore fn dangle() -> &String { // dangle returns a reference to a String @@ -241,7 +243,7 @@ fn dangle() -> &String { // dangle returns a reference to a String // Danger! ``` -因为 `s` 是在 `dangle` 创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的 `String`!这可不对。Rust 不会允许我们这么做的。 +因为 `s` 是在 `dangle` 函数内创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的 `String`!这可不对。Rust 不会允许我们这么做的。 这里的解决方法是直接返回 `String`: @@ -253,15 +255,15 @@ fn no_dangle() -> String { } ``` -这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放掉。 +这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放。 ### 引用的规则 -简要的概括一下对引用的讨论: +让我们简要的概括一下之前对引用的讨论: 1. 在任意给定时间,**只能** 拥有如下中的一个: * 一个可变引用。 * 任意数量的不可变引用。 2. 引用必须总是有效的。 -接下来,我们来看看一种不同类型的引用:slice。 +接下来,我们来看看另一种不同类型的引用:slice。 diff --git a/src/ch04-03-slices.md b/src/ch04-03-slices.md index 47096fb..d5ab5fd 100644 --- a/src/ch04-03-slices.md +++ b/src/ch04-03-slices.md @@ -2,21 +2,21 @@ > [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-03-slices.md) >
-> commit df9e3b922335ec2c76b6c1c4ede31b7742103c48 +> commit 88a12e16d4c7fa669349c9b1ddb48093de92c5e6 另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。 -这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。 +这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应返回。 -让我们看看这个函数的签名: +让我们考虑一下这个函数的签名: ```rust,ignore fn first_word(s: &String) -> ? ``` -`first_word` 这个函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如示例 4-10 所示的代码: +`first_word` 函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。试试如示例 4-5 所示的代码: -Filename: src/main.rs +文件名: src/main.rs ```rust fn first_word(s: &String) -> usize { @@ -32,9 +32,9 @@ fn first_word(s: &String) -> usize { } ``` -示例 4-10:`first_word` 函数返回 `String` 参数的一个字节索引值 +示例 4-5:`first_word` 函数返回 `String` 参数的一个字节索引值 -让我们将代码分解成小块。因为需要一个元素一个元素的检查 `String` 中的值是否是空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组: +让我们将代码分解成小块。因为需要一个元素一个元素的检查 `String` 中的值是否为空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组: ```rust,ignore let bytes = s.as_bytes(); @@ -46,11 +46,11 @@ let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { ``` -第十三章将讨论迭代器的更多细节。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装 `iter` 的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 +第十三章将会讨论迭代器的更多细节。现在,只需知道 `iter` 方法返回集合中的每一个元素,而 `enumerate` 包装 `iter` 的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 -因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在 `for` 循环中,我们指定了一个模式,其中 `i` 是元组中的索引而 `&item` 是单个字节。因为从 `.iter().enumerate()` 中获取了集合元素的引用,我们在模式中使用了`&`。 +因为 `enumerate` 方法返回一个元组,我们可以使用模式来解构,就像 Rust 中其他任何地方所做的一样。所以在 `for` 循环中,我们指定了一个模式,其中 `i` 是元组中的索引而 `&item` 则是单个字节。因为我们从 `.iter().enumerate()` 中获取了集合元素的引用,所以模式中使用了 `&`。 -我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用 `s.len()` 返回字符串的长度: +我们通过字节的字面值语法来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用 `s.len()` 返回字符串的长度: ```rust,ignore if item == b' ' { @@ -60,7 +60,7 @@ for (i, &item) in bytes.iter().enumerate() { s.len() ``` -现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单独一个 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下示例 4-11 中使用了示例 4-10 中 `first_word` 函数的程序: +现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单独一个 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下示例 4-6 中使用了示例 4-5 中 `first_word` 函数的程序: 文件名: src/main.rs @@ -89,9 +89,9 @@ fn main() { } ``` -示例 4-11:储存 `first_word` 函数调用的返回值并接着改变 `String` 的内容 +示例 4-6:储存 `first_word` 函数调用的返回值并接着改变 `String` 的内容 -这个程序编译时没有任何错误,而且在调用 `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` 函数的话,管理索引这件事将更加容易出问题。它的签名看起来像这样: @@ -118,11 +118,11 @@ let world = &s[6..11]; 使用一个由中括号中的 `[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。 -图 4-12 展示了一个图例。 +图 4-6 展示了一个图例。 world containing a pointer to the 6th byte of String s and a length 5 -图 4-12:引用了部分 `String` 的字符串 slice +图 4-6:引用了部分 `String` 的字符串 slice 对于 Rust 的 `..` range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的: @@ -155,6 +155,8 @@ let slice = &s[0..len]; let slice = &s[..]; ``` +> 注意:字符串 slice range 的索引必须位于有效的 UTF-8 字符边界内,如果尝试从一个多字节字符的中间位置创建字符串 slice,则程序将会因错误而退出。出于介绍字符串 slice 的目的,本部分假设只使用 ASCII 字符集;第八章的 “字符串” 部分会更加全面的讨论 UTF-8 处理问题。 + 在记住所有这些知识后,让我们重写 `first_word` 来返回一个 slice。“字符串 slice” 的类型签名写作 `&str`: 文件名: src/main.rs @@ -173,19 +175,19 @@ fn first_word(s: &String) -> &str { } ``` -我们使用跟示例 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。 +我们使用跟示例 4-5 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。 现在当调用 `first_word` 时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。 -`second_word`函数也可以改为返回一个 slice: +`second_word` 函数也可以改为返回一个 slice: ```rust,ignore fn second_word(s: &String) -> &str { ``` -现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向 `String` 的引用保持有效。还记得示例 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上是不正确的,不过却没有表现出任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误: +现在我们有了一个不易混淆且直观的 API 了,因为编译器会确保指向 `String` 的引用持续有效。还记得示例 4-6 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上是不正确的,不过却没有表现出任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore fn main() { @@ -200,19 +202,16 @@ fn main() { 这里是编译错误: ```text -17:6 error: cannot borrow `s` as mutable because it is also borrowed as - immutable [E0502] - s.clear(); // Error! - ^ -15:29 note: previous borrow of `s` occurs here; the immutable borrow prevents - subsequent moves or mutable borrows of `s` until the borrow ends - let word = first_word(&s); - ^ -18:2 note: previous borrow ends here -fn main() { - -} -^ +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable + --> src/main.rs:6:5 + | +4 | let word = first_word(&s); + | - immutable borrow occurs here +5 | +6 | s.clear(); // Error! + | ^ mutable borrow occurs here +7 | } + | - immutable borrow ends here ``` 回忆一下借用规则,当拥有某值的不可变引用时,就不能再获取一个可变引用。因为 `clear` 需要清空 `String`,它尝试获取一个可变引用,它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整类的错误! @@ -274,7 +273,7 @@ fn main() { } ``` -### 其他 slice +### 其他类型的 slice 字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组: @@ -296,4 +295,4 @@ let slice = &a[1..3]; 所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。 -所有权系统影响了 Rust 中其他很多部分如何工作,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个 `struct` 中。 \ No newline at end of file +所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个 `struct` 中。 \ No newline at end of file