From 24fbeec771ab570c4b8ee7ed465052354de64294 Mon Sep 17 00:00:00 2001 From: yang yue Date: Wed, 15 Feb 2017 23:25:41 +0800 Subject: [PATCH] wip --- docs/ch01-02-hello-world.html | 8 +- docs/ch04-01-what-is-ownership.html | 111 ++++- docs/ch04-02-references-and-borrowing.html | 206 +++++++- docs/ch04-03-slices.html | 213 +++++++- docs/print.html | 538 ++++++++++++++++++++- src/ch01-02-hello-world.md | 8 +- src/ch04-01-what-is-ownership.md | 144 +++++- src/ch04-02-references-and-borrowing.md | 283 ++++++++++- src/ch04-03-slices.md | 310 +++++++++++- 9 files changed, 1800 insertions(+), 21 deletions(-) diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index cb9b895..8e79f5e 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -71,14 +71,14 @@

ch01-02-hello-world.md
-commit aa1801d99cd3b19c96533f00c852b1c4bd5350a6

+commit ccbeea7b9fe115cd545881618fe14229d18b307f

现在你已经安装好了 Rust,让我们来编写你的第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分,我们将遵循这个传统。

注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。

-

创建项目文件

-

首先,创建一个文件来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个项目目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:

+

创建项目文件夹

+

首先,创建一个文件夹来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个项目目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:

Linux 和 Mac:

$ mkdir ~/projects
 $ cd ~/projects
@@ -143,7 +143,7 @@ main.rs
 

Hello, Cargo!

Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理它们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖dependencies)。

最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的部分。随着你编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。

-

因为绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

+

由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

$ cargo --version
 

如果看到了版本号,一切 OK!如果出现一个类似“command not found”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。

diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html index 9d29f78..4a5c37b 100644 --- a/docs/ch04-01-what-is-ownership.html +++ b/docs/ch04-01-what-is-ownership.html @@ -229,7 +229,116 @@ which does not implement the `Copy` trait

这样就解决了我们的麻烦!因为只有s2是有效的,当其离开作用域,它就释放自己的内存,完毕。

另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何自动的复制可以被认为对运行时性能影响较小。

变量与数据交互:克隆

-

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone

+

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。

+

这是一个实际使用clone方法的例子:

+
let s1 = String::from("hello");
+let s2 = s1.clone();
+
+println!("s1 = {}, s2 = {}", s1, s2);
+
+

这段代码能正常运行,也是如何显式产生图 4-5 中行为的方式,这里堆上的数据被复制了

+

当出现clone调用时,你知道一些特有的代码被执行而且这些代码可能相当消耗资源。所以它作为一个可视化的标识代表了不同的行为。

+

只在栈上的数据:拷贝

+

这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前列表 4-2 中的一部分:

+
let x = 5;
+let y = x;
+
+println!("x = {}, y = {}", x, y);
+
+

他们似乎与我们刚刚学到的内容向抵触:没有调用clone,不过x依然有效且没有被移动到y中。

+

原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量y后使x无效。换句话说,这里没有深浅拷贝的区别,所以调用clone并不会与通常的浅拷贝有什么不同,我们可以不用管它。

+

Rust 有一个叫做Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有Copy trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy注解,将会出现一个编译时错误。

+

那么什么类型是Copy的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是Copy的,任何不需要分配内存或类似形式资源的类型是Copy的,如下是一些Copy的类型:

+
    +
  • 所有整数类型,比如u32
  • +
  • 布尔类型,bool,它的值是truefalse
  • +
  • 所有浮点数类型,比如f64
  • +
  • 元组,当且仅当其包含的类型也都是Copy的时候。(i32, i32)Copy的,不过(i32, String)就不是。
  • +
+

所有权与函数

+

将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:

+
+Filename: src/main.rs +
fn main() {
+    let s = String::from("hello");  // s comes into scope.
+
+    takes_ownership(s);             // s's value moves into the function...
+                                    // ... and so is no longer valid here.
+    let x = 5;                      // x comes into scope.
+
+    makes_copy(x);                  // x would move into the function,
+                                    // but i32 is Copy, so it’s okay to still
+                                    // use x afterward.
+
+} // Here, x goes out of scope, then s. But since s's value was moved, nothing
+  // special happens.
+
+fn takes_ownership(some_string: String) { // some_string comes into scope.
+    println!("{}", some_string);
+} // Here, some_string goes out of scope and `drop` is called. The backing
+  // memory is freed.
+
+fn makes_copy(some_integer: i32) { // some_integer comes into scope.
+    println!("{}", some_integer);
+} // Here, some_integer goes out of scope. Nothing special happens.
+
+
+

Listing 4-7: Functions with ownership and scope annotated

+
+
+

当尝试在调用takes_ownership后使用s时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在main函数中添加使用sx的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。

+

返回值与作用域

+

返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子:

+

Filename: src/main.rs

+
fn main() {
+    let s1 = gives_ownership();         // gives_ownership moves its return
+                                        // value into s1.
+
+    let s2 = String::from("hello");     // s2 comes into scope.
+
+    let s3 = takes_and_gives_back(s2);  // s2 is moved into
+                                        // takes_and_gives_back, which also
+                                        // moves its return value into s3.
+} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
+  // moved, so nothing happens. s1 goes out of scope and is dropped.
+
+fn gives_ownership() -> String {             // gives_ownership will move its
+                                             // return value into the function
+                                             // that calls it.
+
+    let some_string = String::from("hello"); // some_string comes into scope.
+
+    some_string                              // some_string is returned and
+                                             // moves out to the calling
+                                             // function.
+}
+
+// takes_and_gives_back will take a String and return one.
+fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
+                                                      // scope.
+
+    a_string  // a_string is returned and moves out to the calling function.
+}
+
+

变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当变量值的堆书卷离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过drop被清理掉。

+

在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。

+

使用元组来返回多个值是可能的,像这样:

+

Filename: src/main.rs

+
fn main() {
+    let s1 = String::from("hello");
+
+    let (s2, len) = calculate_length(s1);
+
+    println!("The length of '{}' is {}.", s2, len);
+}
+
+fn calculate_length(s: String) -> (String, usize) {
+    let length = s.len(); // len() returns the length of a String.
+
+    (s, length)
+}
+
+

但是这不免有些形式主义,同时这离一个通用的观点还有很长距离。幸运的是,Rust 对此提供了一个功能,叫做引用references)。

diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html index de5472a..5e9eaba 100644 --- a/docs/ch04-02-references-and-borrowing.html +++ b/docs/ch04-02-references-and-borrowing.html @@ -67,7 +67,211 @@
-

References & Borrowing

+

引用与借用

+
+

ch04-02-references-and-borrowing.md +
+commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+
+

在上一部分的结尾处的使用元组的代码是有问题的,我们需要将String返回给调用者函数这样就可以在调用calculate_length后仍然可以使用String了,因为String先被移动到了calculate_length

+

下面是如何定义并使用一个(新的)calculate_length函数,它以一个对象的引用作为参数而不是获取值的所有权:

+

Filename: src/main.rs

+
fn main() {
+    let s1 = String::from("hello");
+
+    let len = calculate_length(&s1);
+
+    println!("The length of '{}' is {}.", s1, len);
+}
+
+fn calculate_length(s: &String) -> usize {
+    s.len()
+}
+
+

首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递&s1calculate_length,同时在函数定义中,我们获取&String而不是String

+

这些 & 符号就是引用,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。

+
+&String s pointing at String s1 +
+

Figure 4-8: &String s pointing at String s1

+
+
+

仔细看看这个函数调用:

+
# fn calculate_length(s: &String) -> usize {
+#     s.len()
+# }
+let s1 = String::from("hello");
+
+let len = calculate_length(&s1);
+
+

&s1语法允许我们创建一个参考s1的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。

+

同理,函数签名使用了&来表明参数s的类型是一个引用。让我们增加一些解释性的注解:

+
fn calculate_length(s: &String) -> usize { // s is a reference to a String
+    s.len()
+} // Here, s goes out of scope. But because it does not have ownership of what
+  // it refers to, nothing happens.
+
+

变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

+

我们将获取引用作为函数参数称为借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。

+

那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

+
+Filename: src/main.rs +
fn main() {
+    let s = String::from("hello");
+
+    change(&s);
+}
+
+fn change(some_string: &String) {
+    some_string.push_str(", world");
+}
+
+
+

Listing 4-9: Attempting to modify a borrowed value

+
+
+

这里是错误:

+
error: cannot borrow immutable borrowed content `*some_string` as mutable
+ --> error.rs:8:5
+  |
+8 |     some_string.push_str(", world");
+  |     ^^^^^^^^^^^
+
+

正如变量默认是不可变的,引用也一样。不允许修改引用的值。

+

可变引用

+

可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中:

+

Filename: src/main.rs

+
fn main() {
+    let mut s = String::from("hello");
+
+    change(&mut s);
+}
+
+fn change(some_string: &mut String) {
+    some_string.push_str(", world");
+}
+
+

首先,必须将s改为mut。然后必须创建一个可变引用&mut s和接受一个可变引用some_string: &mut String

+

不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:

+

Filename: src/main.rs

+
let mut s = String::from("hello");
+
+let r1 = &mut s;
+let r2 = &mut s;
+
+

具体错误如下:

+
error[E0499]: cannot borrow `s` as mutable more than once at a time
+ --> borrow_twice.rs:5:19
+  |
+4 |     let r1 = &mut s;
+  |                   - first mutable borrow occurs here
+5 |     let r2 = &mut s;
+  |                   ^ second mutable borrow occurs here
+6 | }
+  | - first borrow ends here
+
+

这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。

+

数据竞争是一种特定类型的竞争状态,它可由这三个行为造成:

+
    +
  1. 两个或更多指针同时访问相同的数据。
  2. +
  3. 至少有一个指针被用来写数据。
  4. +
  5. 没有被用来同步数据访问的机制。
  6. +
+

数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

+

一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能同时拥有:

+
let mut s = String::from("hello");
+
+{
+    let r1 = &mut s;
+
+} // r1 goes out of scope here, so we can make a new reference with no problems.
+
+let r2 = &mut s;
+
+

当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误:

+
let mut s = String::from("hello");
+
+let r1 = &s; // no problem
+let r2 = &s; // no problem
+let r3 = &mut s; // BIG PROBLEM
+
+

错误如下:

+
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
+immutable
+ --> borrow_thrice.rs:6:19
+  |
+4 |     let r1 = &s; // no problem
+  |               - immutable borrow occurs here
+5 |     let r2 = &s; // no problem
+6 |     let r3 = &mut s; // BIG PROBLEM
+  |                   ^ mutable borrow occurs here
+7 | }
+  | - immutable borrow ends here
+
+

哇哦!我们不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。

+

即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。

+

悬垂引用

+

在存在指针的语言中,容易错误地生成一个悬垂指针dangling pointer),一个引用某个内存位置的指针,这个内存可能已经因为被分配给别人,因为释放内存时指向内存的指针被保留了下来。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

+

让我们尝试创建一个悬垂引用:

+

Filename: src/main.rs

+
fn main() {
+    let reference_to_nothing = dangle();
+}
+
+fn dangle() -> &String {
+    let s = String::from("hello");
+
+    &s
+}
+
+

这里是错误:

+
error[E0106]: missing lifetime specifier
+ --> dangle.rs:5:16
+  |
+5 | fn dangle() -> &String {
+  |                ^^^^^^^
+  |
+  = 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)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键:

+
this function's return type contains a borrowed value, but there is no value
+for it to be borrowed from.
+
+

让我们仔细看看我们的dangle代码的每一步到底放生了什么:

+
fn dangle() -> &String { // dangle returns a reference to a String
+
+    let s = String::from("hello"); // s is a new String
+
+    &s // we return a reference to the String, s
+} // Here, s goes out of scope, and is dropped. Its memory goes away.
+  // Danger!
+
+

因为s是在dangle创建的,当dangle的代码执行完毕后,s将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的String!这可不好。Rust 不会允许我们这么做的。

+

正确的代码是直接返回String

+
fn no_dangle() -> String {
+    let s = String::from("hello");
+
+    s
+}
+
+

这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放掉。

+

引用的规则

+

简要的概括一下对引用的讨论:

+
    +
  1. 特定时间,只能拥有如下中的一个:
  2. +
+
    +
  • 一个可变引用。
  • +
  • 任意属性的不可变引用。
  • +
+
    +
  1. 引用必须总是有效的。
  2. +
+

接下来,我们来看看一种不同类型的引用:slices。

diff --git a/docs/ch04-03-slices.html b/docs/ch04-03-slices.html index ec0c51c..91bfe91 100644 --- a/docs/ch04-03-slices.html +++ b/docs/ch04-03-slices.html @@ -67,7 +67,218 @@
-

Slices

+

Slices

+
+

ch04-03-slices.md +
+commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+
+

另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

+

这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。

+

让我们看看这个函数的签名:

+
fn first_word(s: &String) -> ?
+
+

first_word这个函数有一个参数&String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取部分字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:

+
+Filename: src/main.rs +
fn first_word(s: &String) -> usize {
+    let bytes = s.as_bytes();
+
+    for (i, &item) in bytes.iter().enumerate() {
+        if item == b' ' {
+            return i;
+        }
+    }
+
+    s.len()
+}
+
+
+

Listing 4-10: The first_word function that returns a byte index value into +the String parameter

+
+
+

让我们将代码分解成小块。因为需要一个元素一个元素的检查String中的值是否是空格,需要用as_bytes方法将String转化为字节数组:

+
let bytes = s.as_bytes();
+
+

Next, we create an iterator over the array of bytes using the iter method :

+
for (i, &item) in bytes.iter().enumerate() {
+
+

第十六章将讨论迭代器的更多细节。现在,只需知道iter方法返回集合中的每一个元素,而enumerate包装iter的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。

+

因为enumerate方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在for循环中,我们指定了一个模式,其中i是元组中的索引而&item是单个字节。因为从.iter().enumerate()中获取了集合元素的引用,我们在模式中使用了&

+

我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用s.len()返回字符串的长度:

+
    if item == b' ' {
+        return i;
+    }
+}
+s.len()
+
+

现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个usize,不过它只在&String的上下文中才是一个有意义的数字。换句话说,因为它是一个与String像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 first_word函数的程序:

+
+Filename: src/main.rs +
# fn first_word(s: &String) -> usize {
+#     let bytes = s.as_bytes();
+#
+#     for (i, &item) in bytes.iter().enumerate() {
+#         if item == b' ' {
+#             return i;
+#         }
+#     }
+#
+#     s.len()
+# }
+#
+fn main() {
+    let mut s = String::from("hello world");
+
+    let word = first_word(&s); // word will get the value 5.
+
+    s.clear(); // This empties the String, making it equal to "".
+
+    // 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!
+}
+
+
+

Listing 4-11: Storing the result from calling the first_word function then +changing the String contents

+
+
+

这个程序编译时没有任何错误,而且在调用s.clear()之后使用word也不会出错。这时words状态就没有联系了,所以word仍然包含值5。可以尝试用值5来提取变量s的第一个单词,不过这是有 bug 的,因为在我们将5保存到word之后s的内容已经改变。

+

不得不担心word的索引与s中的数据不再同步是乏味且容易出错的!如果编写一个second_word函数的话管理索引将更加容易出问题。它的签名看起来像这样:

+
fn second_word(s: &String) -> (usize, usize) {
+
+

现在我们跟踪了一个开始索引一个结尾索引,同时有了更多从数据的某个特定状态计算而来的值,他们也完全没有与这个状态相关联。现在有了三个飘忽不定的不相关变量都需要被同步。

+

幸运的是,Rust 为这个问题提供了一个解决方案:字符串 slice。

+

字符串 slice

+

字符串 slicestring slice)是String中一部分值的引用,它看起来像这样:

+
let s = String::from("hello world");
+
+let hello = &s[0..5];
+let world = &s[6..11];
+
+

这类似于获取整个String的引用不过带有额外的[0..5]部分。不同于整个String的引用,这是一个包含String内部的一个位置和所需元素数量的引用。

+

我们使用一个 range [starting_index..ending_index]来创建 slice,不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就let world = &s[6..11];来说,world将是一个包含指向s第 6 个字节的指针和长度值 5 的 slice。

+

图 4-12 展示了一个图例

+
+world containing a pointer to the 6th byte of String s and a length 5 +
+

Figure 4-12: String slice referring to part of a String

+
+
+

对于 Rust 的.. range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:

+
let s = String::from("hello");
+
+let slice = &s[0..2];
+let slice = &s[..2];
+
+

由此类推,如果 slice 包含String的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:

+
let s = String::from("hello");
+
+let len = s.len();
+
+let slice = &s[0..len];
+let slice = &s[..];
+
+

在记住所有这些知识后,让我们重写first_word来返回一个 slice。“字符串 slice”的签名写作&str

+

Filename: src/main.rs

+
fn first_word(s: &String) -> &str {
+    let bytes = s.as_bytes();
+
+    for (i, &item) in bytes.iter().enumerate() {
+        if item == b' ' {
+            return &s[0..i];
+        }
+    }
+
+    &s[..]
+}
+
+

我们使用跟列表 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。

+

现在当调用first_word时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。

+

second_word函数也可以改为返回一个 slice:

+
fn second_word(s: &String) -> &str {
+
+

现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向String的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上时不正确的,不过却没有任何直观的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的first_word会抛出一个编译时错误:

+

Filename: src/main.rs

+
fn main() {
+    let mut s = String::from("hello world");
+
+    let word = first_word(&s);
+
+    s.clear(); // Error!
+}
+
+

这里是编译错误:

+
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() {
+
+}
+^
+
+

回忆一下借用规则,当拥有某值的不可变引用时。不能再获取一个可变引用。因为clear需要清空String,它尝试获取一个可变引用,它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整个错误类型!

+

字符串字面值就是 slice

+

还记得我们讲到过字符串字面值被储存在二进制文件中吗。现在知道 slice 了,我们就可以正确的理解字符串字面值了:

+
let s = "Hello, world!";
+
+

这里s的类型是&str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str是一个不可变引用。

+

字符串 slice 作为参数

+

在知道了能够获取字面值和String的 slice 后引起了另一个对first_word的改进,这是它的签名:

+
fn first_word(s: &String) -> &str {
+
+

相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对String&str使用相同的函数:

+
fn first_word(s: &str) -> &str {
+
+

如果有一个字符串 slice,可以直接传递它。如果有一个String,则可以传递整个String的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能:

+

Filename: src/main.rs

+
# fn first_word(s: &str) -> &str {
+#     let bytes = s.as_bytes();
+#
+#     for (i, &item) in bytes.iter().enumerate() {
+#         if item == b' ' {
+#             return &s[0..i];
+#         }
+#     }
+#
+#     &s[..]
+# }
+fn main() {
+    let my_string = String::from("hello world");
+
+    // first_word works on slices of `String`s
+    let word = first_word(&my_string[..]);
+
+    let my_string_literal = "hello world";
+
+    // first_word works on slices of string literals
+    let word = first_word(&my_string_literal[..]);
+
+    // since string literals *are* string slices already,
+    // this works too, without the slice syntax!
+    let word = first_word(my_string_literal);
+}
+
+

其他 slice

+

字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组:

+
let a = [1, 2, 3, 4, 5];
+
+

就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分,而我们可以这样做:

+
let a = [1, 2, 3, 4, 5];
+
+let slice = &a[1..3];
+
+

这个 slice 的类型是&[i32]。它跟以跟字符串 slice 一样的方式工作,通过储存第一个元素的引用和一个长度。你可以对其他所有类型的集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。

+

总结

+

所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。

+

所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个struct中。

diff --git a/docs/print.html b/docs/print.html index 87752c5..4a29c86 100644 --- a/docs/print.html +++ b/docs/print.html @@ -122,14 +122,14 @@ commit f828919e62aa542aaaae03c1fb565da42374213e

ch01-02-hello-world.md
-commit aa1801d99cd3b19c96533f00c852b1c4bd5350a6

+commit ccbeea7b9fe115cd545881618fe14229d18b307f

现在你已经安装好了 Rust,让我们来编写你的第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分,我们将遵循这个传统。

注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。

-

创建项目文件

-

首先,创建一个文件来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个项目目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:

+

创建项目文件夹

+

首先,创建一个文件夹来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个项目目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹:

Linux 和 Mac:

$ mkdir ~/projects
 $ cd ~/projects
@@ -194,7 +194,7 @@ main.rs
 

Hello, Cargo!

Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Cargo 来管理它们的 Rust 项目,因为它使得很多任务变得更轻松。例如,Cargo负责构建代码、下载代码依赖的库并编译这些库。我们把代码需要的库叫做 依赖dependencies)。

最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的部分。随着你编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。

-

因为绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

+

由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo:

$ cargo --version
 

如果看到了版本号,一切 OK!如果出现一个类似“command not found”的错误,那么你应该查看安装方式的文档来确定如何单独安装 Cargo。

@@ -1767,9 +1767,533 @@ which does not implement the `Copy` trait

这样就解决了我们的麻烦!因为只有s2是有效的,当其离开作用域,它就释放自己的内存,完毕。

另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的“深拷贝”。因此,任何自动的复制可以被认为对运行时性能影响较小。

变量与数据交互:克隆

-

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone

-

References & Borrowing

-

Slices

+

如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。

+

这是一个实际使用clone方法的例子:

+
let s1 = String::from("hello");
+let s2 = s1.clone();
+
+println!("s1 = {}, s2 = {}", s1, s2);
+
+

这段代码能正常运行,也是如何显式产生图 4-5 中行为的方式,这里堆上的数据被复制了

+

当出现clone调用时,你知道一些特有的代码被执行而且这些代码可能相当消耗资源。所以它作为一个可视化的标识代表了不同的行为。

+

只在栈上的数据:拷贝

+

这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前列表 4-2 中的一部分:

+
let x = 5;
+let y = x;
+
+println!("x = {}, y = {}", x, y);
+
+

他们似乎与我们刚刚学到的内容向抵触:没有调用clone,不过x依然有效且没有被移动到y中。

+

原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量y后使x无效。换句话说,这里没有深浅拷贝的区别,所以调用clone并不会与通常的浅拷贝有什么不同,我们可以不用管它。

+

Rust 有一个叫做Copy trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有Copy trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了Drop trait 的类型使用Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用Copy注解,将会出现一个编译时错误。

+

那么什么类型是Copy的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是Copy的,任何不需要分配内存或类似形式资源的类型是Copy的,如下是一些Copy的类型:

+
    +
  • 所有整数类型,比如u32
  • +
  • 布尔类型,bool,它的值是truefalse
  • +
  • 所有浮点数类型,比如f64
  • +
  • 元组,当且仅当其包含的类型也都是Copy的时候。(i32, i32)Copy的,不过(i32, String)就不是。
  • +
+

所有权与函数

+

将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:

+
+Filename: src/main.rs +
fn main() {
+    let s = String::from("hello");  // s comes into scope.
+
+    takes_ownership(s);             // s's value moves into the function...
+                                    // ... and so is no longer valid here.
+    let x = 5;                      // x comes into scope.
+
+    makes_copy(x);                  // x would move into the function,
+                                    // but i32 is Copy, so it’s okay to still
+                                    // use x afterward.
+
+} // Here, x goes out of scope, then s. But since s's value was moved, nothing
+  // special happens.
+
+fn takes_ownership(some_string: String) { // some_string comes into scope.
+    println!("{}", some_string);
+} // Here, some_string goes out of scope and `drop` is called. The backing
+  // memory is freed.
+
+fn makes_copy(some_integer: i32) { // some_integer comes into scope.
+    println!("{}", some_integer);
+} // Here, some_integer goes out of scope. Nothing special happens.
+
+
+

Listing 4-7: Functions with ownership and scope annotated

+
+
+

当尝试在调用takes_ownership后使用s时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在main函数中添加使用sx的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。

+

返回值与作用域

+

返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子:

+

Filename: src/main.rs

+
fn main() {
+    let s1 = gives_ownership();         // gives_ownership moves its return
+                                        // value into s1.
+
+    let s2 = String::from("hello");     // s2 comes into scope.
+
+    let s3 = takes_and_gives_back(s2);  // s2 is moved into
+                                        // takes_and_gives_back, which also
+                                        // moves its return value into s3.
+} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
+  // moved, so nothing happens. s1 goes out of scope and is dropped.
+
+fn gives_ownership() -> String {             // gives_ownership will move its
+                                             // return value into the function
+                                             // that calls it.
+
+    let some_string = String::from("hello"); // some_string comes into scope.
+
+    some_string                              // some_string is returned and
+                                             // moves out to the calling
+                                             // function.
+}
+
+// takes_and_gives_back will take a String and return one.
+fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
+                                                      // scope.
+
+    a_string  // a_string is returned and moves out to the calling function.
+}
+
+

变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当变量值的堆书卷离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过drop被清理掉。

+

在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。

+

使用元组来返回多个值是可能的,像这样:

+

Filename: src/main.rs

+
fn main() {
+    let s1 = String::from("hello");
+
+    let (s2, len) = calculate_length(s1);
+
+    println!("The length of '{}' is {}.", s2, len);
+}
+
+fn calculate_length(s: String) -> (String, usize) {
+    let length = s.len(); // len() returns the length of a String.
+
+    (s, length)
+}
+
+

但是这不免有些形式主义,同时这离一个通用的观点还有很长距离。幸运的是,Rust 对此提供了一个功能,叫做引用references)。

+

引用与借用

+
+

ch04-02-references-and-borrowing.md +
+commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+
+

在上一部分的结尾处的使用元组的代码是有问题的,我们需要将String返回给调用者函数这样就可以在调用calculate_length后仍然可以使用String了,因为String先被移动到了calculate_length

+

下面是如何定义并使用一个(新的)calculate_length函数,它以一个对象的引用作为参数而不是获取值的所有权:

+

Filename: src/main.rs

+
fn main() {
+    let s1 = String::from("hello");
+
+    let len = calculate_length(&s1);
+
+    println!("The length of '{}' is {}.", s1, len);
+}
+
+fn calculate_length(s: &String) -> usize {
+    s.len()
+}
+
+

首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递&s1calculate_length,同时在函数定义中,我们获取&String而不是String

+

这些 & 符号就是引用,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。

+
+&String s pointing at String s1 +
+

Figure 4-8: &String s pointing at String s1

+
+
+

仔细看看这个函数调用:

+
# fn calculate_length(s: &String) -> usize {
+#     s.len()
+# }
+let s1 = String::from("hello");
+
+let len = calculate_length(&s1);
+
+

&s1语法允许我们创建一个参考s1的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。

+

同理,函数签名使用了&来表明参数s的类型是一个引用。让我们增加一些解释性的注解:

+
fn calculate_length(s: &String) -> usize { // s is a reference to a String
+    s.len()
+} // Here, s goes out of scope. But because it does not have ownership of what
+  // it refers to, nothing happens.
+
+

变量s有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。

+

我们将获取引用作为函数参数称为借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。

+

那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!

+
+Filename: src/main.rs +
fn main() {
+    let s = String::from("hello");
+
+    change(&s);
+}
+
+fn change(some_string: &String) {
+    some_string.push_str(", world");
+}
+
+
+

Listing 4-9: Attempting to modify a borrowed value

+
+
+

这里是错误:

+
error: cannot borrow immutable borrowed content `*some_string` as mutable
+ --> error.rs:8:5
+  |
+8 |     some_string.push_str(", world");
+  |     ^^^^^^^^^^^
+
+

正如变量默认是不可变的,引用也一样。不允许修改引用的值。

+

可变引用

+

可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中:

+

Filename: src/main.rs

+
fn main() {
+    let mut s = String::from("hello");
+
+    change(&mut s);
+}
+
+fn change(some_string: &mut String) {
+    some_string.push_str(", world");
+}
+
+

首先,必须将s改为mut。然后必须创建一个可变引用&mut s和接受一个可变引用some_string: &mut String

+

不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:

+

Filename: src/main.rs

+
let mut s = String::from("hello");
+
+let r1 = &mut s;
+let r2 = &mut s;
+
+

具体错误如下:

+
error[E0499]: cannot borrow `s` as mutable more than once at a time
+ --> borrow_twice.rs:5:19
+  |
+4 |     let r1 = &mut s;
+  |                   - first mutable borrow occurs here
+5 |     let r2 = &mut s;
+  |                   ^ second mutable borrow occurs here
+6 | }
+  | - first borrow ends here
+
+

这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。

+

数据竞争是一种特定类型的竞争状态,它可由这三个行为造成:

+
    +
  1. 两个或更多指针同时访问相同的数据。
  2. +
  3. 至少有一个指针被用来写数据。
  4. +
  5. 没有被用来同步数据访问的机制。
  6. +
+

数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译!

+

一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能同时拥有:

+
let mut s = String::from("hello");
+
+{
+    let r1 = &mut s;
+
+} // r1 goes out of scope here, so we can make a new reference with no problems.
+
+let r2 = &mut s;
+
+

当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误:

+
let mut s = String::from("hello");
+
+let r1 = &s; // no problem
+let r2 = &s; // no problem
+let r3 = &mut s; // BIG PROBLEM
+
+

错误如下:

+
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
+immutable
+ --> borrow_thrice.rs:6:19
+  |
+4 |     let r1 = &s; // no problem
+  |               - immutable borrow occurs here
+5 |     let r2 = &s; // no problem
+6 |     let r3 = &mut s; // BIG PROBLEM
+  |                   ^ mutable borrow occurs here
+7 | }
+  | - immutable borrow ends here
+
+

哇哦!我们不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。

+

即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。

+

悬垂引用

+

在存在指针的语言中,容易错误地生成一个悬垂指针dangling pointer),一个引用某个内存位置的指针,这个内存可能已经因为被分配给别人,因为释放内存时指向内存的指针被保留了下来。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

+

让我们尝试创建一个悬垂引用:

+

Filename: src/main.rs

+
fn main() {
+    let reference_to_nothing = dangle();
+}
+
+fn dangle() -> &String {
+    let s = String::from("hello");
+
+    &s
+}
+
+

这里是错误:

+
error[E0106]: missing lifetime specifier
+ --> dangle.rs:5:16
+  |
+5 | fn dangle() -> &String {
+  |                ^^^^^^^
+  |
+  = 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)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键:

+
this function's return type contains a borrowed value, but there is no value
+for it to be borrowed from.
+
+

让我们仔细看看我们的dangle代码的每一步到底放生了什么:

+
fn dangle() -> &String { // dangle returns a reference to a String
+
+    let s = String::from("hello"); // s is a new String
+
+    &s // we return a reference to the String, s
+} // Here, s goes out of scope, and is dropped. Its memory goes away.
+  // Danger!
+
+

因为s是在dangle创建的,当dangle的代码执行完毕后,s将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的String!这可不好。Rust 不会允许我们这么做的。

+

正确的代码是直接返回String

+
fn no_dangle() -> String {
+    let s = String::from("hello");
+
+    s
+}
+
+

这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放掉。

+

引用的规则

+

简要的概括一下对引用的讨论:

+
    +
  1. 特定时间,只能拥有如下中的一个:
  2. +
+
    +
  • 一个可变引用。
  • +
  • 任意属性的不可变引用。
  • +
+
    +
  1. 引用必须总是有效的。
  2. +
+

接下来,我们来看看一种不同类型的引用:slices。

+

Slices

+
+

ch04-03-slices.md +
+commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+
+

另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

+

这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。

+

让我们看看这个函数的签名:

+
fn first_word(s: &String) -> ?
+
+

first_word这个函数有一个参数&String。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取部分字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:

+
+Filename: src/main.rs +
fn first_word(s: &String) -> usize {
+    let bytes = s.as_bytes();
+
+    for (i, &item) in bytes.iter().enumerate() {
+        if item == b' ' {
+            return i;
+        }
+    }
+
+    s.len()
+}
+
+
+

Listing 4-10: The first_word function that returns a byte index value into +the String parameter

+
+
+

让我们将代码分解成小块。因为需要一个元素一个元素的检查String中的值是否是空格,需要用as_bytes方法将String转化为字节数组:

+
let bytes = s.as_bytes();
+
+

Next, we create an iterator over the array of bytes using the iter method :

+
for (i, &item) in bytes.iter().enumerate() {
+
+

第十六章将讨论迭代器的更多细节。现在,只需知道iter方法返回集合中的每一个元素,而enumerate包装iter的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。

+

因为enumerate方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在for循环中,我们指定了一个模式,其中i是元组中的索引而&item是单个字节。因为从.iter().enumerate()中获取了集合元素的引用,我们在模式中使用了&

+

我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用s.len()返回字符串的长度:

+
    if item == b' ' {
+        return i;
+    }
+}
+s.len()
+
+

现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个usize,不过它只在&String的上下文中才是一个有意义的数字。换句话说,因为它是一个与String像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 first_word函数的程序:

+
+Filename: src/main.rs +
# fn first_word(s: &String) -> usize {
+#     let bytes = s.as_bytes();
+#
+#     for (i, &item) in bytes.iter().enumerate() {
+#         if item == b' ' {
+#             return i;
+#         }
+#     }
+#
+#     s.len()
+# }
+#
+fn main() {
+    let mut s = String::from("hello world");
+
+    let word = first_word(&s); // word will get the value 5.
+
+    s.clear(); // This empties the String, making it equal to "".
+
+    // 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!
+}
+
+
+

Listing 4-11: Storing the result from calling the first_word function then +changing the String contents

+
+
+

这个程序编译时没有任何错误,而且在调用s.clear()之后使用word也不会出错。这时words状态就没有联系了,所以word仍然包含值5。可以尝试用值5来提取变量s的第一个单词,不过这是有 bug 的,因为在我们将5保存到word之后s的内容已经改变。

+

不得不担心word的索引与s中的数据不再同步是乏味且容易出错的!如果编写一个second_word函数的话管理索引将更加容易出问题。它的签名看起来像这样:

+
fn second_word(s: &String) -> (usize, usize) {
+
+

现在我们跟踪了一个开始索引一个结尾索引,同时有了更多从数据的某个特定状态计算而来的值,他们也完全没有与这个状态相关联。现在有了三个飘忽不定的不相关变量都需要被同步。

+

幸运的是,Rust 为这个问题提供了一个解决方案:字符串 slice。

+

字符串 slice

+

字符串 slicestring slice)是String中一部分值的引用,它看起来像这样:

+
let s = String::from("hello world");
+
+let hello = &s[0..5];
+let world = &s[6..11];
+
+

这类似于获取整个String的引用不过带有额外的[0..5]部分。不同于整个String的引用,这是一个包含String内部的一个位置和所需元素数量的引用。

+

我们使用一个 range [starting_index..ending_index]来创建 slice,不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就let world = &s[6..11];来说,world将是一个包含指向s第 6 个字节的指针和长度值 5 的 slice。

+

图 4-12 展示了一个图例

+
+world containing a pointer to the 6th byte of String s and a length 5 +
+

Figure 4-12: String slice referring to part of a String

+
+
+

对于 Rust 的.. range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的:

+
let s = String::from("hello");
+
+let slice = &s[0..2];
+let slice = &s[..2];
+
+

由此类推,如果 slice 包含String的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:

+
let s = String::from("hello");
+
+let len = s.len();
+
+let slice = &s[0..len];
+let slice = &s[..];
+
+

在记住所有这些知识后,让我们重写first_word来返回一个 slice。“字符串 slice”的签名写作&str

+

Filename: src/main.rs

+
fn first_word(s: &String) -> &str {
+    let bytes = s.as_bytes();
+
+    for (i, &item) in bytes.iter().enumerate() {
+        if item == b' ' {
+            return &s[0..i];
+        }
+    }
+
+    &s[..]
+}
+
+

我们使用跟列表 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。

+

现在当调用first_word时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。

+

second_word函数也可以改为返回一个 slice:

+
fn second_word(s: &String) -> &str {
+
+

现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向String的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上时不正确的,不过却没有任何直观的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的first_word会抛出一个编译时错误:

+

Filename: src/main.rs

+
fn main() {
+    let mut s = String::from("hello world");
+
+    let word = first_word(&s);
+
+    s.clear(); // Error!
+}
+
+

这里是编译错误:

+
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() {
+
+}
+^
+
+

回忆一下借用规则,当拥有某值的不可变引用时。不能再获取一个可变引用。因为clear需要清空String,它尝试获取一个可变引用,它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整个错误类型!

+

字符串字面值就是 slice

+

还记得我们讲到过字符串字面值被储存在二进制文件中吗。现在知道 slice 了,我们就可以正确的理解字符串字面值了:

+
let s = "Hello, world!";
+
+

这里s的类型是&str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str是一个不可变引用。

+

字符串 slice 作为参数

+

在知道了能够获取字面值和String的 slice 后引起了另一个对first_word的改进,这是它的签名:

+
fn first_word(s: &String) -> &str {
+
+

相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对String&str使用相同的函数:

+
fn first_word(s: &str) -> &str {
+
+

如果有一个字符串 slice,可以直接传递它。如果有一个String,则可以传递整个String的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能:

+

Filename: src/main.rs

+
# fn first_word(s: &str) -> &str {
+#     let bytes = s.as_bytes();
+#
+#     for (i, &item) in bytes.iter().enumerate() {
+#         if item == b' ' {
+#             return &s[0..i];
+#         }
+#     }
+#
+#     &s[..]
+# }
+fn main() {
+    let my_string = String::from("hello world");
+
+    // first_word works on slices of `String`s
+    let word = first_word(&my_string[..]);
+
+    let my_string_literal = "hello world";
+
+    // first_word works on slices of string literals
+    let word = first_word(&my_string_literal[..]);
+
+    // since string literals *are* string slices already,
+    // this works too, without the slice syntax!
+    let word = first_word(my_string_literal);
+}
+
+

其他 slice

+

字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组:

+
let a = [1, 2, 3, 4, 5];
+
+

就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分,而我们可以这样做:

+
let a = [1, 2, 3, 4, 5];
+
+let slice = &a[1..3];
+
+

这个 slice 的类型是&[i32]。它跟以跟字符串 slice 一样的方式工作,通过储存第一个元素的引用和一个长度。你可以对其他所有类型的集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。

+

总结

+

所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。

+

所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个struct中。

diff --git a/src/ch01-02-hello-world.md b/src/ch01-02-hello-world.md index a9147ab..ff3ff2c 100644 --- a/src/ch01-02-hello-world.md +++ b/src/ch01-02-hello-world.md @@ -2,15 +2,15 @@ > [ch01-02-hello-world.md](https://github.com/rust-lang/book/blob/master/src/ch01-02-hello-world.md) >
-> commit aa1801d99cd3b19c96533f00c852b1c4bd5350a6 +> commit ccbeea7b9fe115cd545881618fe14229d18b307f 现在你已经安装好了 Rust,让我们来编写你的第一个 Rust 程序。当学习一门新语言的时候,编写一个在屏幕上打印 “Hello, world!” 文本的小程序是一个传统,而在这一部分,我们将遵循这个传统。 > 注意:本书假设你熟悉基本的命令行操作。Rust 本身并不对你的编辑器,工具和你的代码存放在何处有什么特定的要求,所以如果你比起命令行更喜欢 IDE,请随意选择你喜欢的 IDE。 -### 创建项目文件 +### 创建项目文件夹 -首先,创建一个文件来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个**项目**目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹: +首先,创建一个文件夹来编写 Rust 代码。Rust 并不关心你的代码存放在哪里,不过在本书中,我们建议在你的 home 目录创建一个**项目**目录,并把你的所有项目放在这。打开一个终端并输入如下命令来为这个项目创建一个文件夹: Linux 和 Mac: @@ -125,7 +125,7 @@ Cargo 是 Rust 的构建系统和包管理工具,同时 Rustacean 们使用 Ca 最简单的 Rust 程序,例如我们刚刚编写的,并没有任何依赖,所以目前我们只使用了 Cargo 负责构建代码的部分。随着你编写更加复杂的 Rust 程序,你会想要添加依赖,那么如果你使用 Cargo 开始的话,这将会变得简单许多。 -因为绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo: +由于绝大部分 Rust 项目使用 Cargo,本书接下来的部分将假设你使用它。如果使用安装章节介绍的官方安装包的话,Rust 自带 Cargo。如果通过其他方式安装 Rust 的话,可以在终端输入如下命令检查是否安装了 Cargo: ```sh $ cargo --version diff --git a/src/ch04-01-what-is-ownership.md b/src/ch04-01-what-is-ownership.md index 4c9890b..d86a879 100644 --- a/src/ch04-01-what-is-ownership.md +++ b/src/ch04-01-what-is-ownership.md @@ -248,4 +248,146 @@ Figure 4-6: Representation in memory after `s1` has been invalidated #### 变量与数据交互:克隆 -如果我们**确实**需要深度复制`String`中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做`clone` \ No newline at end of file +如果我们**确实**需要深度复制`String`中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做`clone`的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。 + +这是一个实际使用`clone`方法的例子: + +```rust +let s1 = String::from("hello"); +let s2 = s1.clone(); + +println!("s1 = {}, s2 = {}", s1, s2); +``` + +这段代码能正常运行,也是如何显式产生图 4-5 中行为的方式,这里堆上的数据**被复制了**。 + +当出现`clone`调用时,你知道一些特有的代码被执行而且这些代码可能相当消耗资源。所以它作为一个可视化的标识代表了不同的行为。 + +#### 只在栈上的数据:拷贝 + +这里还有一个没有提到的小窍门。这些代码使用了整型并且是有效的,他们是之前列表 4-2 中的一部分: + +```rust +let x = 5; +let y = x; + +println!("x = {}, y = {}", x, y); +``` + +他们似乎与我们刚刚学到的内容向抵触:没有调用`clone`,不过`x`依然有效且没有被移动到`y`中。 + +原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量`y`后使`x`无效。换句话说,这里没有深浅拷贝的区别,所以调用`clone`并不会与通常的浅拷贝有什么不同,我们可以不用管它。 + +Rust 有一个叫做`Copy` trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait)。如果一个类型拥有`Copy` trait,一个旧的变量在(重新)赋值后仍然可用。Rust 不允许自身或其任何部分实现了`Drop` trait 的类型使用`Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用`Copy`注解,将会出现一个编译时错误。 + +那么什么类型是`Copy`的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是`Copy`的,任何不需要分配内存或类似形式资源的类型是`Copy`的,如下是一些`Copy`的类型: + +* 所有整数类型,比如`u32`。 +* 布尔类型,`bool`,它的值是`true`和`false`。 +* 所有浮点数类型,比如`f64`。 +* 元组,当且仅当其包含的类型也都是`Copy`的时候。`(i32, i32)`是`Copy`的,不过`(i32, String)`就不是。 + +### 所有权与函数 + +将值传递给函数在语言上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子: + +
+Filename: src/main.rs + +```rust +fn main() { + let s = String::from("hello"); // s comes into scope. + + takes_ownership(s); // s's value moves into the function... + // ... and so is no longer valid here. + let x = 5; // x comes into scope. + + makes_copy(x); // x would move into the function, + // but i32 is Copy, so it’s okay to still + // use x afterward. + +} // Here, x goes out of scope, then s. But since s's value was moved, nothing + // special happens. + +fn takes_ownership(some_string: String) { // some_string comes into scope. + println!("{}", some_string); +} // Here, some_string goes out of scope and `drop` is called. The backing + // memory is freed. + +fn makes_copy(some_integer: i32) { // some_integer comes into scope. + println!("{}", some_integer); +} // Here, some_integer goes out of scope. Nothing special happens. +``` + +
+ +Listing 4-7: Functions with ownership and scope annotated + +
+
+ +当尝试在调用`takes_ownership`后使用`s`时,Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在`main`函数中添加使用`s`和`x`的代码来看看哪里能使用他们,和哪里所有权规则会阻止我们这么做。 + +### 返回值与作用域 + +返回值也可以转移作用域。这里是一个有与列表 4-7 中类似标注的例子: + +Filename: src/main.rs + +```rust +fn main() { + let s1 = gives_ownership(); // gives_ownership moves its return + // value into s1. + + let s2 = String::from("hello"); // s2 comes into scope. + + let s3 = takes_and_gives_back(s2); // s2 is moved into + // takes_and_gives_back, which also + // moves its return value into s3. +} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was + // moved, so nothing happens. s1 goes out of scope and is dropped. + +fn gives_ownership() -> String { // gives_ownership will move its + // return value into the function + // that calls it. + + let some_string = String::from("hello"); // some_string comes into scope. + + some_string // some_string is returned and + // moves out to the calling + // function. +} + +// takes_and_gives_back will take a String and return one. +fn takes_and_gives_back(a_string: String) -> String { // a_string comes into + // scope. + + a_string // a_string is returned and moves out to the calling function. +} +``` + +变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,并且当变量值的堆书卷离开作用域时,如果数据的所有权没有被移动到另外一个变量时,其值将通过`drop`被清理掉。 + +在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。 + +使用元组来返回多个值是可能的,像这样: + +Filename: src/main.rs + +```rust +fn main() { + let s1 = String::from("hello"); + + let (s2, len) = calculate_length(s1); + + println!("The length of '{}' is {}.", s2, len); +} + +fn calculate_length(s: String) -> (String, usize) { + let length = s.len(); // len() returns the length of a String. + + (s, length) +} +``` + +但是这不免有些形式主义,同时这离一个通用的观点还有很长距离。幸运的是,Rust 对此提供了一个功能,叫做**引用**(*references*)。 \ No newline at end of file diff --git a/src/ch04-02-references-and-borrowing.md b/src/ch04-02-references-and-borrowing.md index 5f0552e..5324646 100644 --- a/src/ch04-02-references-and-borrowing.md +++ b/src/ch04-02-references-and-borrowing.md @@ -1 +1,282 @@ -# References & Borrowing +## 引用与借用 + +> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/src/ch04-02-references-and-borrowing.md) +>
+> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c + +在上一部分的结尾处的使用元组的代码是有问题的,我们需要将`String`返回给调用者函数这样就可以在调用`calculate_length`后仍然可以使用`String`了,因为`String`先被移动到了`calculate_length`。 + +下面是如何定义并使用一个(新的)`calculate_length`函数,它以一个对象的**引用**作为参数而不是获取值的所有权: + +Filename: src/main.rs + +```rust +fn main() { + let s1 = String::from("hello"); + + let len = calculate_length(&s1); + + println!("The length of '{}' is {}.", s1, len); +} + +fn calculate_length(s: &String) -> usize { + s.len() +} +``` + +首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递`&s1`给`calculate_length`,同时在函数定义中,我们获取`&String`而不是`String`。 + +这些 & 符号就是**引用**,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。 + + +
+&String s pointing at String s1 + +
+ +Figure 4-8: `&String s` pointing at `String s1` + +
+
+ +仔细看看这个函数调用: + +```rust +# fn calculate_length(s: &String) -> usize { +# s.len() +# } +let s1 = String::from("hello"); + +let len = calculate_length(&s1); +``` + +`&s1`语法允许我们创建一个**参考**值`s1`的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。 + +同理,函数签名使用了`&`来表明参数`s`的类型是一个引用。让我们增加一些解释性的注解: + +```rust +fn calculate_length(s: &String) -> usize { // s is a reference to a String + s.len() +} // Here, s goes out of scope. But because it does not have ownership of what + // it refers to, nothing happens. +``` + +变量`s`有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有它。 + +我们将获取引用作为函数参数称为**借用**(*borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从它哪里借来。当你使用完毕,必须还回去。 + +那么如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通! + +
+Filename: src/main.rs + +```rust,ignore +fn main() { + let s = String::from("hello"); + + change(&s); +} + +fn change(some_string: &String) { + some_string.push_str(", world"); +} +``` + +
+ +Listing 4-9: Attempting to modify a borrowed value + +
+
+ +这里是错误: + +```sh +error: cannot borrow immutable borrowed content `*some_string` as mutable + --> error.rs:8:5 + | +8 | some_string.push_str(", world"); + | ^^^^^^^^^^^ +``` + +正如变量默认是不可变的,引用也一样。不允许修改引用的值。 + +### 可变引用 + +可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中: + +Filename: src/main.rs + +```rust +fn main() { + let mut s = String::from("hello"); + + change(&mut s); +} + +fn change(some_string: &mut String) { + some_string.push_str(", world"); +} +``` + +首先,必须将`s`改为`mut`。然后必须创建一个可变引用`&mut s`和接受一个可变引用`some_string: &mut String`。 + +不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败: + +Filename: src/main.rs + +```rust,ignore +let mut s = String::from("hello"); + +let r1 = &mut s; +let r2 = &mut s; +``` + +具体错误如下: + +```text +error[E0499]: cannot borrow `s` as mutable more than once at a time + --> borrow_twice.rs:5:19 + | +4 | let r1 = &mut s; + | - first mutable borrow occurs here +5 | let r2 = &mut s; + | ^ second mutable borrow occurs here +6 | } + | - first borrow ends here +``` + +这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争(data races)。 + +**数据竞争**是一种特定类型的竞争状态,它可由这三个行为造成: + +1. 两个或更多指针同时访问相同的数据。 +2. 至少有一个指针被用来写数据。 +3. 没有被用来同步数据访问的机制。 + +数据竞争会导致未定义行为并且当在运行时尝试追踪时可能会变得难以诊断和修复;Rust 阻止了这种情况的发生,因为存在数据竞争的代码根本就不能编译! + +一如既往,使用大括号来创建一个新的作用域,允许拥有多个可变引用,只是不能**同时**拥有: + +```rust +let mut s = String::from("hello"); + +{ + let r1 = &mut s; + +} // r1 goes out of scope here, so we can make a new reference with no problems. + +let r2 = &mut s; +``` + +当结合可变和不可变引用时有一个类似的规则存在。这些代码会导致一个错误: + + +```rust,ignore +let mut s = String::from("hello"); + +let r1 = &s; // no problem +let r2 = &s; // no problem +let r3 = &mut s; // BIG PROBLEM +``` + +错误如下: + +```sh +error[E0502]: cannot borrow `s` as mutable because it is also borrowed as +immutable + --> borrow_thrice.rs:6:19 + | +4 | let r1 = &s; // no problem + | - immutable borrow occurs here +5 | let r2 = &s; // no problem +6 | let r3 = &mut s; // BIG PROBLEM + | ^ mutable borrow occurs here +7 | } + | - immutable borrow ends here +``` + +哇哦!我们**也**不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。 + +即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug(在编译时而不是运行时)并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。 + +### 悬垂引用 + +在存在指针的语言中,容易错误地生成一个**悬垂指针**(*dangling pointer*),一个引用某个内存位置的指针,这个内存可能已经因为被分配给别人,因为释放内存时指向内存的指针被保留了下来。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。 + +让我们尝试创建一个悬垂引用: + + +Filename: src/main.rs + +```rust,ignore +fn main() { + let reference_to_nothing = dangle(); +} + +fn dangle() -> &String { + let s = String::from("hello"); + + &s +} +``` + +这里是错误: + +``` +error[E0106]: missing lifetime specifier + --> dangle.rs:5:16 + | +5 | fn dangle() -> &String { + | ^^^^^^^ + | + = 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*)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键: + +``` +this function's return type contains a borrowed value, but there is no value +for it to be borrowed from. +``` + +让我们仔细看看我们的`dangle`代码的每一步到底放生了什么: + +```rust,ignore +fn dangle() -> &String { // dangle returns a reference to a String + + let s = String::from("hello"); // s is a new String + + &s // we return a reference to the String, s +} // Here, s goes out of scope, and is dropped. Its memory goes away. + // Danger! +``` + +因为`s`是在`dangle`创建的,当`dangle`的代码执行完毕后,`s`将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的`String`!这可不好。Rust 不会允许我们这么做的。 + +正确的代码是直接返回`String`: + +```rust +fn no_dangle() -> String { + let s = String::from("hello"); + + s +} +``` + +这样就可以没有任何错误的运行了。所有权被移动出去,所以没有值被释放掉。 + +### 引用的规则 + +简要的概括一下对引用的讨论: + +1. 特定时间,**只能**拥有如下中的一个: + * 一个可变引用。 + * 任意属性的不可变引用。 +2. 引用必须总是有效的。 + +接下来,我们来看看一种不同类型的引用:slices。 \ No newline at end of file diff --git a/src/ch04-03-slices.md b/src/ch04-03-slices.md index 26d2b55..e201d3c 100644 --- a/src/ch04-03-slices.md +++ b/src/ch04-03-slices.md @@ -1 +1,309 @@ -# Slices +## Slices + +> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/src/ch04-03-slices.md) +>
+> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c + +另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。 + +这里有一个小的编程问题:编写一个获取一个字符串并返回它在其中找到的第一个单词的函数。如果函数没有在字符串中找到一个空格,就意味着整个字符串是一个单词,所以整个字符串都应该返回。 + +让我们看看这个函数的签名: + +```rust,ignore +fn first_word(s: &String) -> ? +``` + +`first_word`这个函数有一个参数`&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取**部分**字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码: + +
+Filename: src/main.rs + +```rust +fn first_word(s: &String) -> usize { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + if item == b' ' { + return i; + } + } + + s.len() +} +``` + +
+ +Listing 4-10: The `first_word` function that returns a byte index value into +the `String` parameter + +
+
+ +让我们将代码分解成小块。因为需要一个元素一个元素的检查`String`中的值是否是空格,需要用`as_bytes`方法将`String`转化为字节数组: + +```rust,ignore +let bytes = s.as_bytes(); +``` + +Next, we create an iterator over the array of bytes using the `iter` method : + +```rust,ignore +for (i, &item) in bytes.iter().enumerate() { +``` + +第十六章将讨论迭代器的更多细节。现在,只需知道`iter`方法返回集合中的每一个元素,而`enumerate`包装`iter`的结果并返回一个元组,其中每一个元素是元组的一部分。返回元组的第一个元素是索引,第二个元素是集合中元素的引用。这比我们自己计算索引要方便一些。 + +因为`enumerate`方法返回一个元组,我们可以使用模式来解构它,就像 Rust 中其他地方一样。所以在`for`循环中,我们指定了一个模式,其中`i`是元组中的索引而`&item`是单个字节。因为从`.iter().enumerate()`中获取了集合元素的引用,我们在模式中使用了`&`。 + +我们通过字节的字面值来寻找代表空格的字节。如果找到了,返回它的位置。否则,使用`s.len()`返回字符串的长度: + +```rust,ignore + if item == b' ' { + return i; + } +} +s.len() +``` + +现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个`usize`,不过它只在`&String`的上下文中才是一个有意义的数字。换句话说,因为它是一个与`String`像分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 `first_word`函数的程序: + +
+Filename: src/main.rs + +```rust +# fn first_word(s: &String) -> usize { +# let bytes = s.as_bytes(); +# +# for (i, &item) in bytes.iter().enumerate() { +# if item == b' ' { +# return i; +# } +# } +# +# s.len() +# } +# +fn main() { + let mut s = String::from("hello world"); + + let word = first_word(&s); // word will get the value 5. + + s.clear(); // This empties the String, making it equal to "". + + // 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! +} +``` + +
+ +Listing 4-11: Storing the result from calling the `first_word` function then +changing the `String` contents + +
+
+ +这个程序编译时没有任何错误,而且在调用`s.clear()`之后使用`word`也不会出错。这时`word`与`s`状态就没有联系了,所以`word`仍然包含值`5`。可以尝试用值`5`来提取变量`s`的第一个单词,不过这是有 bug 的,因为在我们将`5`保存到`word`之后`s`的内容已经改变。 + +不得不担心`word`的索引与`s`中的数据不再同步是乏味且容易出错的!如果编写一个`second_word`函数的话管理索引将更加容易出问题。它的签名看起来像这样: + +```rust,ignore +fn second_word(s: &String) -> (usize, usize) { +``` + +现在我们跟踪了一个开始索引**和**一个结尾索引,同时有了更多从数据的某个特定状态计算而来的值,他们也完全没有与这个状态相关联。现在有了三个飘忽不定的不相关变量都需要被同步。 + +幸运的是,Rust 为这个问题提供了一个解决方案:字符串 slice。 + +### 字符串 slice + +**字符串 slice**(*string slice*)是`String`中一部分值的引用,它看起来像这样: + +```rust +let s = String::from("hello world"); + +let hello = &s[0..5]; +let world = &s[6..11]; +``` + +这类似于获取整个`String`的引用不过带有额外的`[0..5]`部分。不同于整个`String`的引用,这是一个包含`String`内部的一个位置和所需元素数量的引用。 + +我们使用一个 range `[starting_index..ending_index]`来创建 slice,不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就`let world = &s[6..11];`来说,`world`将是一个包含指向`s`第 6 个字节的指针和长度值 5 的 slice。 + +图 4-12 展示了一个图例 + + +
+world containing a pointer to the 6th byte of String s and a length 5 + +
+ +Figure 4-12: String slice referring to part of a `String` + +
+
+ +对于 Rust 的`..` range 语法,如果想要从第一个索引(0)开始,可以不写两个点号之前的值。换句话说,如下两个语句是相同的: + +```rust +let s = String::from("hello"); + +let slice = &s[0..2]; +let slice = &s[..2]; +``` + +由此类推,如果 slice 包含`String`的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的: + +```rust +let s = String::from("hello"); + +let len = s.len(); + +let slice = &s[0..len]; +let slice = &s[..]; +``` + +在记住所有这些知识后,让我们重写`first_word`来返回一个 slice。“字符串 slice”的签名写作`&str`: + +Filename: src/main.rs + +```rust +fn first_word(s: &String) -> &str { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + if item == b' ' { + return &s[0..i]; + } + } + + &s[..] +} +``` + +我们使用跟列表 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。 + +现在当调用`first_word`时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。 + +`second_word`函数也可以改为返回一个 slice: + +```rust,ignore +fn second_word(s: &String) -> &str { +``` + +现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向`String`的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上时不正确的,不过却没有任何直观的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的`first_word`会抛出一个编译时错误: + +Filename: src/main.rs + +```rust,ignore +fn main() { + let mut s = String::from("hello world"); + + let word = first_word(&s); + + s.clear(); // Error! +} +``` + +这里是编译错误: + +``` +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() { + +} +^ +``` + +回忆一下借用规则,当拥有某值的不可变引用时。不能再获取一个可变引用。因为`clear`需要清空`String`,它尝试获取一个可变引用,它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整个错误类型! + +#### 字符串字面值就是 slice + +还记得我们讲到过字符串字面值被储存在二进制文件中吗。现在知道 slice 了,我们就可以正确的理解字符串字面值了: + +```rust +let s = "Hello, world!"; +``` + +这里`s`的类型是`&str`:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;`&str`是一个不可变引用。 + +#### 字符串 slice 作为参数 + +在知道了能够获取字面值和`String`的 slice 后引起了另一个对`first_word`的改进,这是它的签名: + +```rust,ignore +fn first_word(s: &String) -> &str { +``` + +相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对`String`和`&str`使用相同的函数: + +```rust,ignore +fn first_word(s: &str) -> &str { +``` + +如果有一个字符串 slice,可以直接传递它。如果有一个`String`,则可以传递整个`String`的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能: + +Filename: src/main.rs + +```rust +# fn first_word(s: &str) -> &str { +# let bytes = s.as_bytes(); +# +# for (i, &item) in bytes.iter().enumerate() { +# if item == b' ' { +# return &s[0..i]; +# } +# } +# +# &s[..] +# } +fn main() { + let my_string = String::from("hello world"); + + // first_word works on slices of `String`s + let word = first_word(&my_string[..]); + + let my_string_literal = "hello world"; + + // first_word works on slices of string literals + let word = first_word(&my_string_literal[..]); + + // since string literals *are* string slices already, + // this works too, without the slice syntax! + let word = first_word(my_string_literal); +} +``` + +### 其他 slice + +字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组: + +```rust +let a = [1, 2, 3, 4, 5]; +``` + +就跟我们想要获取字符串的一部分那样,我们也会想要引用数组的一部分,而我们可以这样做: + +```rust +let a = [1, 2, 3, 4, 5]; + +let slice = &a[1..3]; +``` + +这个 slice 的类型是`&[i32]`。它跟以跟字符串 slice 一样的方式工作,通过储存第一个元素的引用和一个长度。你可以对其他所有类型的集合使用这类 slice。第八章讲到 vector 时会详细讨论这些集合。 + +## 总结 + +所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。 + +所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个`struct`中。 \ No newline at end of file