check ch05-00

pull/69/head
KaiserY 7 years ago
parent 9eb6f98334
commit cf22db5871

@ -4,4 +4,4 @@
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
所有权(系统)是 Rust 最独特的功能,它使得 Rust 可以无需垃圾回收garbage collector就能保障内存安全。因此理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能借用、slice 以及 Rust 如何在内存中摆放数据。
所有权(系统)是 Rust 最独特的功能,它使得 Rust 可以无需垃圾回收garbage collector就能保障内存安全。因此理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能借用、slice 以及 Rust 如何在内存中布局数据。

@ -2,9 +2,9 @@
> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-01-what-is-ownership.md)
> <br>
> commit 6d4ef020095a375483b2121d4fa2b1661062cc92
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
Rust 的核心功能(之一)是**所有权***ownership*)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。
Rust 的核心功能(之一)是 **所有权***ownership*)。虽然这个功能理解起来很直观,不过它对语言的其余部分有着更深层的含义。
所有程序都必须管理他们运行时使用计算机内存的方式。一些语言中使用垃圾回收在程序运行过程中来时刻寻找不再被使用的内存在另一些语言中程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:内存被一个所有权系统管理,它拥有一系列的规则使编译器在编译时进行检查。任何所有权系统的功能都不会导致运行时开销。
@ -18,15 +18,15 @@ Rust 的核心功能(之一)是**所有权***ownership*)。虽然这个
>
> 在很多语言中并不经常需要考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的选择。我们会在本章的稍后部分描述所有权与堆与栈相关的部分,所以这里只是一个用来预热的简要解释。
>
> 栈和堆都是代码在运行时可供使用的内存部分,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作**后进先出***last in, first out*)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做**进栈***pushing onto the stack*),而移出数据叫做**出栈***popping off the stack*)。
> 栈和堆都是代码在运行时可供使用的部分内存,不过他们以不同的结构组成。栈以放入值的顺序存储并以相反顺序取出值。这也被称作 **后进先出***last in, first out*)。想象一下一叠盘子:当增加更多盘子时,把他们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 **进栈***pushing onto the stack*),而移出数据叫做 **出栈***popping off the stack*)。
>
> 操作栈是非常快的,因为它访问数据的方式:永远也不需要寻找一个位置放入新数据或者取出数据因为这个位置总是在栈顶。另一个使得栈快速的性质是栈中的所有数据都必须是一个已知的固定的大小。
>
> 相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个它位置的指针。这个过程称作**在堆上分配内存***allocating on the heap*并且有时这个过程就简称为“分配”allocating。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的我们可以将指针储存在栈上不过当需要实际数据时必须访问指针。
> 相反对于在编译时未知大小或大小可能变化的数据,可以把他们储存在堆上。堆是缺乏组织的:当向堆放入数据时,我们请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回给我们一个它位置的指针。这个过程称作 **在堆上分配内存***allocating on the heap*),并且有时这个过程就简称为 “分配”allocating。向栈中放入数据并不被认为是分配。因为指针是已知的固定大小的我们可以将指针储存在栈上不过当需要实际数据时必须访问指针。
>
> 想象一下去餐馆就坐吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。
>
> 访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代的处理器在内存中跳转越少就越快。继续类比,假设有一台服务器来处理来自多个桌子的订单。它在处理完一个桌子的所有订单后再移动到下一个桌子是最有效率的。从桌子 A 获取一个订单,接着再从桌子 B 获取一个订单,然后再从桌子 A然后再从桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
> 访问堆上的数据要比访问栈上的数据要慢因为必须通过指针来访问。现代的处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一台服务器来处理来自多个桌子的订单。它在处理完一个桌子的所有订单后再移动到下一个桌子是最有效率的。从桌子 A 获取一个订单,接着再从桌子 B 获取一个订单,然后再从桌子 A然后再从桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据之间彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
>
> 当调用一个函数,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
>
@ -38,21 +38,21 @@ Rust 的核心功能(之一)是**所有权***ownership*)。虽然这个
首先,让我们看一下所有权的规则。请记住它们,我们将讲解一些它们的例子:
> 1. 每一个值都被它的**所有者***owner*)变量拥有。
> 1. 每一个值都被它的 **所有者***owner*)变量拥有。
> 2. 值在任意时刻只能被一个所有者拥有。
> 3. 当所有者离开作用域,这个值将被丢弃。
### 变量作用域
我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在所有的例子中包含`fn main() {`代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个`main`函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。
我们已经在第二章完成过一个 Rust 程序的例子。现在我们已经掌握了基本语法,所以不会在之后的例子中包含 `fn main() {` 代码了,所以如果你是一路跟过来的,必须手动将之后例子的代码放入一个`main`函数中。为此,例子将显得更加具体,使我们可以关注具体细节而不是样板代码。
作为所有权的第一个例子,我们看看一些变量的**作用域***scope*)。作用域是一个项(原文:item)在程序中有效的范围。假如有一个这样的变量:
作为所有权的第一个例子,我们看看一些变量的 **作用域***scope*)。作用域是一个项(原文:item) 在程序中有效的范围。假如有一个这样的变量:
```rust
let s = "hello";
```
变量`s`绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前*作用域*结束时都是有效的。列表 4-1 的注释标明了变量`s`在哪里是有效的:
变量`s`绑定到了一个字符串字面值,这个字符串值是硬编码进我们程序代码中的。这个变量从声明的点开始直到当前 **作用域** 结束时都是有效的。列表 4-1 的注释标明了变量`s`在哪里是有效的:
```rust
{ // s is not valid here, its not yet declared
@ -62,31 +62,30 @@ let s = "hello";
} // this scope is now over, and s is no longer valid
```
<span class="caption">Listing 4-1: A variable and the scope in which it is
valid</span>
<span class="caption">列表 4-1一个变量和其有效的作用域</span>
换句话说,这里有两个重要的点:
1. 当`s`**进入作用域**,它就是有效的。
2. 这一直持续到它**离开作用域**为止。
1. 当`s` **进入作用域**,它就是有效的。
2. 这一直持续到它 **离开作用域** 为止。
目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍`String`类型。
目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍 `String` 类型。
### `String`类型
### `String` 类型
为了演示所有权的规则,我们需要一个比第三章讲到的任何一个都要复杂的数据类型。之前出现的数据类型都是储存在栈上的并且当离开作用域时被移出栈,不过我们需要寻找一个储存在堆上的数据来探索 Rust 如何知道该在何时清理数据的。
这里使用`String`作为例子并专注于`String`与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解`String`。
这里使用 `String` 作为例子并专注于 `String` 与所有权相关的部分。这些方面也同样适用于其他标准库提供的或你自己创建的复杂数据类型。在第八章会更深入地讲解 `String`
我们已经见过字符串字面值了它被硬编码进程序里。字符串字面值是很方便的不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道例如如果想要获取用户输入并储存该怎么办呢为此Rust 有第二个字符串类型,`String`。这个类型储存在堆上所以储存在编译时未知大小的文本。可以用`from`从字符串字面值来创建`String`,如下:
我们已经见过字符串字面值了它被硬编码进程序里。字符串字面值是很方便的不过他们并不总是适合所有需要使用文本的场景。原因之一就是他们是不可变的。另一个原因是不是所有字符串的值都能在编写代码时就知道例如如果想要获取用户输入并储存该怎么办呢为此Rust 有第二个字符串类型,`String`。这个类型储存在堆上所以能够储存在编译时未知大小的文本。可以用 `from` 从字符串字面值来创建 `String`,如下:
```rust
let s = String::from("hello");
```
这两个冒号(`::`)运算符允许将特定的`from`函数置于`String`类型的命名空间namespace下而不需要使用类似`string_from`这样的名字。在第五章的“方法语法”“Method Syntax”部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。
这两个冒号(`::`)运算符允许将特定的 `from` 函数置于 `String` 类型的命名空间namespace下而不需要使用类似 `string_from` 这样的名字。在第五章的 “方法语法”“Method Syntax”部分会着重讲解这个语法而且在第七章会讲到模块的命名空间。
这类字符串*可以*被修改:
这类字符串 *可以* 被修改:
```rust
let mut s = String::from("hello");
@ -96,22 +95,22 @@ s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`
```
那么这里有什么区别呢?为什么`String`可变而字面值却不行呢?区别在于两个类型对内存的处理上。
那么这里有什么区别呢?为什么 `String` 可变而字面值却不行呢?区别在于两个类型对内存的处理上。
### 内存与分配
对于字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于它的不可变形。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。
对于字符串字面值的情况,我们在编译时就知道内容所以它直接被硬编码进最终的可执行文件中,这使得字符串字面值快速和高效。不过这些属性都只来源于其不可变性。不幸的是,我们不能为了每一个在编译时未知大小的文本而将一块内存放入二进制文件中而它的大小还可能随着程序运行而改变。
对于`String`类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
对于 `String` 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
1. 内存必须在运行时向操作系统请求
2. 需要一个当我们处理完`String`时将内存返回给操作系统的方法
1. 内存必须在运行时向操作系统请求
2. 需要一个当我们处理完 `String` 时将内存返回给操作系统的方法
第一部分由我们完成:当调用`String::from`时,它的实现请求它需要的内存。这在编程语言中是非常通用的。
第一部分由我们完成:当调用 `String::from` 时,它的实现请求它需要的内存。这在编程语言中是非常通用的。
然而,第二部分实现起来就各有区别了。在有**垃圾回收***GC*)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要`allocate`和`free`一一对应。
然而,第二部分实现起来就各有区别了。在有 **垃圾回收***garbage collector**GC*)的语言中, GC 记录并清除不再使用的内存,而我们作为程序员,并不需要关心他们。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们程序员的责任了,正如请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要 `allocate` `free` 一一对应。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用`String`而不是字符串字面值的版本:
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。下面是列表 4-1 中作用域例子的一个使用 `String` 而不是字符串字面值的版本:
```rust
{
@ -122,13 +121,13 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
// longer valid
```
这里是一个将`String`需要的内存返回给操作系统的很自然的位置:当`s`离开作用域的时候。当变量离开作用域Rust 为其调用一个特殊的函数。这个函数叫做 `drop`,在这里`String`的作者可以放置释放内存的代码。Rust 在结尾的`}`处自动调用`drop`。
这里是一个将 `String` 需要的内存返回给操作系统的很自然的位置:当 `s` 离开作用域的时候。当变量离开作用域Rust 为其调用一个特殊的函数。这个函数叫做 `drop`,在这里 `String` 的作者可以放置释放内存的代码。Rust 在结尾的 `}` 处自动调用 `drop`
> 注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作**资源获取即初始化***Resource Acquisition Is Initialization (RAII)*)。如果你使用过 RAII 模式的话应该对 Rust 的`drop`函数不陌生。
> 注意:在 C++ 中,这种 item 在生命周期结束时释放资源的方法有时被称作 **资源获取即初始化***Resource Acquisition Is Initialization (RAII)*)。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数不陌生。
这个模式对编写 Rust 代码的方式有着深远的影响。它现在看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。
#### 变量与数据交互:移动
#### 变量与数据交互的方式(一):移动
Rust 中的多个变量以一种独特的方式与同一数据交互。让我们看看列表 4-2 中一个使用整型的例子:
@ -137,57 +136,53 @@ let x = 5;
let y = x;
```
<span class="caption">Listing 4-2: Assigning the integer value of variable `x`
to `y`</span>
<span class="caption">列表 4-2将变量 `x` 赋值给 `y`</span>
根据其他语言的经验大致可以猜到这在干什么:“将`5`绑定到`x`;接着生成一个值`x`的拷贝并绑定到`y`”。现在有了两个变量,`x`和`y`,都等于`5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个`5`被放入了栈中。
根据其他语言的经验大致可以猜到这在干什么:“将 `5` 绑定到 `x`;接着生成一个值 `x` 的拷贝并绑定到 `y`”。现在有了两个变量,`x` `y`,都等于 `5`。这也正是事实上发生了的,因为正数是有已知固定大小的简单值,所以这两个 `5` 被放入了栈中。
现在看看这个`String`版本:
现在看看这个 `String` 版本:
```rust
let s1 = String::from("hello");
let s2 = s1;
```
这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个`s1`的拷贝并绑定到`s2`上。不过,事实上并不完全是这样。
这看起来与上面的代码非常类似,所以我们可能会假设他们的运行方式也是类似的:也就是说,第二行可能会生成一个 `s1` 的拷贝并绑定到 `s2` 上。不过,事实上并不完全是这样。
为了更全面的解释这个问题,让我们看看图 4-3 中`String`真正是什么样。`String`由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。
为了更全面的解释这个问题,让我们看看图 4-3 中 `String` 真正是什么样。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据储存在栈上。右侧则是堆上存放内容的内存部分。
<img alt="String in memory" src="img/trpl04-01.svg" class="center" style="width: 50%;" />
<span class="caption">Figure 4-3: Representation in memory of a `String`
holding the value `"hello"` bound to `s1`</span>
<span class="caption">图 4-3一个绑定到 `s1` 的拥有值 `"hello"``String` 的内存表现</span>
长度代表当前`String`的内容使用了多少字节的内存。容量是`String`从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。
长度代表当前 `String` 的内容使用了多少字节的内存。容量是 `String` 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过这在目前为止的场景中并不重要,所以可以暂时忽略容量。
当我们把`s1`赋值给`s2``String`的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。
当我们把 `s1` 赋值给 `s2``String` 的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制堆上指针所指向的数据。换句话说,内存中数据的表现如图 4-4 所示。
<img alt="s1 and s2 pointing to the same value" src="img/trpl04-02.svg" class="center" style="width: 50%;" />
<span class="caption">Figure 4-4: Representation in memory of the variable `s2`
that has a copy of the pointer, length, and capacity of `s1`</span>
<span class="caption">图 4-4变量 `s2` 的内存表现,它有一份 `s1` 指针、长度和容量的拷贝</span>
这个表现形式看起来**并不像**图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作`s2 = s1`在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。
这个表现形式看起来 **并不像** 图 4-5 中的那样,它是如果 Rust 也拷贝了堆上的数据后内存看起来是怎么样的。如果 Rust 这么做了,那么操作 `s2 = s1` 在堆上数据比较大的时候可能会对运行时性能造成非常大的影响。
<img alt="s1 and s2 to two places" src="img/trpl04-03.svg" class="center" style="width: 50%;" />
<span class="caption">Figure 4-5: Another possibility of what `s2 = s1` might
do if Rust copied the heap data as well</span>
<span class="caption">图 4-5另一个 `s2 = s1` 时可能的内存表现,如果 Rust 同时也拷贝了堆上的数据的话</span>
之前,我们提到过当变量离开作用域后 Rust 自动调用`drop`函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当`s2`和`s1`离开作用域,他们都会尝试释放相同的内存。这是一个叫做 *double free* 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
之前,我们提到过当变量离开作用域后 Rust 自动调用 `drop` 函数并清理变量的堆内存。不过图 4-4 展示了两个数据指针指向了同一位置。这就有了一个问题:当 `s2` `s1` 离开作用域,他们都会尝试释放相同的内存。这是一个叫做 *double free* 的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存Rust 则认为`s1`不再有效,因此 Rust 不需要在`s1`离开作用域后清理任何东西。看看在`s2`被创建之后尝试使用`s1`会发生生么:
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存Rust 则认为 `s1` 不再有效,因此 Rust 不需要在 `s1` 离开作用域后清理任何东西。看看在 `s2` 被创建之后尝试使用 `s1` 会发生生么:
```rust,ignore
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
println!("{}, world!", s1);
```
你会得到一个类似如下的错误,因为 Rust 禁止你使用无效的引用。
```sh
```text
error[E0382]: use of moved value: `s1`
--> src/main.rs:4:27
|
@ -200,22 +195,21 @@ error[E0382]: use of moved value: `s1`
which does not implement the `Copy` trait
```
如果你在其他语言中听说过术语“浅拷贝”“shallow copy”和“深拷贝”“deep copy”那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被称为**移动***move*),而不是浅拷贝。上面的例子可以解读为`s1`被**移动**到了`s2`中。那么具体发生了什么如图 4-6 所示。
如果你在其他语言中听说过术语 “浅拷贝”“shallow copy” “深拷贝”“deep copy”那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效化了,这个操作被称为 **移动***move*),而不是浅拷贝。上面的例子可以解读为 `s1` **移动** 到了 `s2` 中。那么具体发生了什么如图 4-6 所示。
<img alt="s1 moved to s2" src="img/trpl04-04.svg" class="center" style="width: 50%;" />
<span class="caption">Figure 4-6: Representation in memory after `s1` has been
invalidated</span>
<span class="caption">图 4-6`s1` 无效化之后的内存表现</span>
这样就解决了我们的麻烦!因为只有`s2`是有效的,当其离开作用域,它就释放自己的内存,完毕。
这样就解决了我们的麻烦!因为只有 `s2` 是有效的,当其离开作用域,它就释放自己的内存,完毕。
另外这里还隐含了一个设计选择Rust 永远也不会自动创建数据的“深拷贝”。因此,任何**自动**的复制可以被认为对运行时性能影响较小。
另外这里还隐含了一个设计选择Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 **自动** 的复制可以被认为对运行时性能影响较小。
#### 变量与数据交互:克隆
#### 变量与数据交互的方式(二):克隆
如果我们**确实**需要深度复制`String`中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做`clone`的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。
如果我们 **确实** 需要深度复制 `String` 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 `clone` 的通用函数。第五章会讨论方法语法,不过因为方法在很多语言中是一个常见功能,所以之前你可能已经见过了。
这是一个实际使用`clone`方法的例子:
这是一个实际使用 `clone` 方法的例子:
```rust
let s1 = String::from("hello");
@ -224,9 +218,9 @@ let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
```
这段代码能正常运行,也是如何显式产生图 4-5 中行为的方式,这里堆上的数据**被复制了**
这段代码能正常运行,也是如何显式产生图 4-5 中行为的方式,这里堆上的数据 **确实** 被复制了。
当出现`clone`调用时,你知道一些特有的代码被执行而且这些代码可能相当消耗资源。所以它作为一个可视化的标识代表了不同的行为。
当出现 `clone` 调用时,你知道一些特有的代码被执行而且这些代码可能相当消耗资源。它作为一个代表发生了不同的行为的可视化的标识
#### 只在栈上的数据:拷贝
@ -239,24 +233,24 @@ let y = x;
println!("x = {}, y = {}", x, y);
```
他们似乎与我们刚刚学到的内容相抵触:没有调用`clone`,不过`x`依然有效且没有被移动到`y`中。
他们似乎与我们刚刚学到的内容相抵触:没有调用 `clone`,不过 `x` 依然有效且没有被移动到 `y` 中。
原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量`y`后使`x`无效。换句话说,这里没有深浅拷贝的区别,所以调用`clone`并不会与通常的浅拷贝有什么不同,我们可以不用管它。
原因是像整型这样的在编译时已知大小的类型被整个储存在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 `y` 后使 `x` 无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 `clone` 并不会与通常的浅拷贝有什么不同,我们可以不用管它。
Rust 有一个叫做`Copy` trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait。如果一个类型拥有`Copy` trait一个旧的变量在重新赋值后仍然可用。Rust 不允许自身或其任何部分实现了`Drop` trait 的类型使用`Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用`Copy`注解,将会出现一个编译时错误。关于如何为你的类型增加`Copy`注解,请阅读附录 C 中的 Derivable Trait。
Rust 有一个叫做 `Copy` trait 的特殊注解,可以用在类似整型这样的储存在栈上的类型(第十章详细讲解 trait。如果一个类型拥有 `Copy` trait一个旧的变量在重新赋值后仍然可用。Rust 不允许自身或其任何部分实现了 `Drop` trait 的类型使用`Copy` trait。如果我们对其值离开作用域时需要特殊处理的类型使用 `Copy` 注解,将会出现一个编译时错误。关于如何为你的类型增加 `Copy` 注解,请阅读附录 C 中的可导出 trait。
那么什么类型是`Copy`的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是`Copy`的,任何需要分配内存,或者本身就是某种形式资源的类型不会是`Copy`的。如下是一些`Copy`的类型:
那么什么类型是 `Copy` 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则,任何简单标量值的组合可以是 `Copy` 的,任何需要分配内存,或者本身就是某种形式资源的类型不会是 `Copy `的。如下是一些 `Copy` 的类型:
* 所有整数类型,比如`u32`。
* 布尔类型,`bool`,它的值是`true`和`false`。
* 所有浮点数类型,比如`f64`。
* 元组,当且仅当其包含的类型也都是`Copy`的时候。`(i32, i32)`是`Copy`的,不过`(i32, String)`就不是。
* 所有整数类型,比如 `u32`
* 布尔类型,`bool`,它的值是 `true` `false`
* 所有浮点数类型,比如 `f64`
* 元组,当且仅当其包含的类型也都是 `Copy` 的时候。`(i32, i32)` `Copy` 的,不过 `(i32, String)` 就不是。
### 所有权与函数
将值传递给函数在语上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:
将值传递给函数在语上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。列表 4-7 是一个带有变量何时进入和离开作用域标注的例子:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
fn main() {
@ -283,10 +277,9 @@ fn makes_copy(some_integer: i32) { // some_integer comes into scope.
} // Here, some_integer goes out of scope. Nothing special happens.
```
<span class="caption">Listing 4-7: Functions with ownership and scope
annotated</span>
<span class="caption">列表 4-7带有所有权和作用域标注的函数</span>
当尝试在调用`takes_ownership`后使用`s`时Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在`main`函数中添加使用`s`和`x`的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。
当尝试在调用 `takes_ownership` 后使用 `s` Rust 会抛出一个编译时错误。这些静态检查使我们免于犯错。试试在 `main` 函数中添加使用 `s` `x` 的代码来看看哪里能使用他们,以及哪里所有权规则会阻止我们这么做。
### 返回值与作用域
@ -326,13 +319,13 @@ fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
}
```
变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过`drop`被清理掉,除非数据被移动为另一个变量所有。
变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 `drop` 被清理掉,除非数据被移动为另一个变量所有。
在每一个函数中都获取并接着返回所有权是冗余乏味的。如果我们想要函数使用一个值但不获取所有权改怎么办呢?如果我们还要接着使用它的话,每次都传递出去再传回来就有点烦人了,另外我们也可能想要返回函数体产生的任何(不止一个)数据。
使用元组来返回多个值是可能的,像这样:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
fn main() {
@ -350,4 +343,4 @@ fn calculate_length(s: String) -> (String, usize) {
}
```
但是这不免有些形式主义同时这离一个通用的观点还有很长距离。幸运的是Rust 对此提供了一个功能,叫做**引用***references*)。
但是这不免有些形式主义同时这离一个通用的观点还有很长距离。幸运的是Rust 对此提供了一个功能,叫做 **引用***references*)。

@ -2,11 +2,11 @@
> [ch04-02-references-and-borrowing.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-02-references-and-borrowing.md)
> <br>
> commit 5e0546f53cce14b126527d9ba6d1b8eb212b4f3d
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
在上一部分的结尾处的使用元组的代码是有问题的,我们需要将`String`返回给调用者函数这样就可以在调用`calculate_length`后仍然可以使用`String`了,因为`String`先被移动到了`calculate_length`。
在上一部分的结尾处的使用元组的代码是有问题的,我们需要将 `String` 返回给调用者函数这样就可以在调用 `calculate_length` 后仍然可以使用 `String` 了,因为 `String` 先被移动到了 `calculate_length`
下面是如何定义并使用一个(新的)`calculate_length`函数,它以一个对象的**引用**作为参数而不是获取值的所有权:
下面是如何定义并使用一个(新的)`calculate_length` 函数,它以一个对象的 **引用** 作为参数而不是获取值的所有权:
<span class="filename">Filename: src/main.rs</span>
@ -24,13 +24,13 @@ fn calculate_length(s: &String) -> usize {
}
```
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递`&s1`给`calculate_length`,同时在函数定义中,我们获取`&String`而不是`String`。
首先,注意变量声明和函数返回值中的所有元组代码都消失了。其次,注意我们传递 `&s1` `calculate_length`,同时在函数定义中,我们获取 `&String` 而不是 `String`
这些 & 符号就是**引用**,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。
这些 & 符号就是 **引用**,他们允许你使用值但不获取它的所有权。图 4-8 展示了一个图解。
<img alt="&String s pointing at String s1" src="img/trpl04-05.svg" class="center" />
<span class="caption">Figure 4-8: `&String s` pointing at `String s1`</span>
<span class="caption">图 4-8`&String s` 指向 `String s1`</span>
仔细看看这个函数调用:
@ -43,9 +43,9 @@ let s1 = String::from("hello");
let len = calculate_length(&s1);
```
`&s1`语法允许我们创建一个**参考**值`s1`的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。
`&s1` 语法允许我们创建一个 **参考** `s1` 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域它指向的值也不会被丢弃。
同理,函数签名使用了`&`来表明参数`s`的类型是一个引用。让我们增加一些解释性的注解:
同理,函数签名使用了 `&` 来表明参数 `s` 的类型是一个引用。让我们增加一些解释性的注解:
```rust
fn calculate_length(s: &String) -> usize { // s is a reference to a String
@ -54,13 +54,13 @@ fn calculate_length(s: &String) -> usize { // s is a reference to a String
// it refers to, nothing happens.
```
变量`s`有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有
变量 `s` 有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据因为我们没有所有权。函数使用引用而不是实际值作为参数意味着无需返回值来交还所有权,因为就不曾拥有所有权
我们将获取引用作为函数参数称为**借用***borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
我们将获取引用作为函数参数称为 **借用***borrowing*)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。
如果我们尝试修改借用的变量呢?尝试列表 4-9 中的代码。剧透:这行不通!
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
fn main() {
@ -74,11 +74,11 @@ fn change(some_string: &String) {
}
```
<span class="caption">Listing 4-9: Attempting to modify a borrowed value</span>
<span class="caption">列表 4-9尝试修改借用的值</span>
这里是错误:
```
```text
error: cannot borrow immutable borrowed content `*some_string` as mutable
--> error.rs:8:5
|
@ -92,7 +92,7 @@ error: cannot borrow immutable borrowed content `*some_string` as mutable
可以通过一个小调整来修复在列表 4-9 代码中的错误,在列表 4-9 的代码中:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
fn main() {
@ -106,11 +106,11 @@ fn change(some_string: &mut String) {
}
```
首先,必须将`s`改为`mut`。然后必须创建一个可变引用`&mut s`和接受一个可变引用`some_string: &mut String`。
首先,必须将 `s` 改为 `mut`。然后必须创建一个可变引用 `&mut s` 和接受一个可变引用 `some_string: &mut String`
不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
let mut s = String::from("hello");
@ -119,9 +119,9 @@ 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
|
@ -133,17 +133,17 @@ error[E0499]: cannot borrow `s` as mutable more than once at a time
| - first borrow ends here
```
这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争data races
这个限制允许可变性,不过是以一种受限制的方式。新 Rustacean 们经常与此作斗争,因为大部分语言任何时候变量都是可变的。这个限制的好处是 Rust 可以在编译时就避免数据竞争。
**数据竞争**是一种特定类型的竞争状态,它可由这三个行为造成:
**数据竞争***data race*是一种特定类型的竞争状态,它可由这三个行为造成:
1. 两个或更多指针同时访问同一数据。
2. 至少有一个指针被写入。
3. 没有同步数据访问的机制。
2. 至少有一个这样的指针被用来写入数据
3. 不存在同步数据访问的机制。
数据竞争会导致未定义行为,在运行时难以追踪并且难以诊断和修复Rust 避免了这种情况,它拒绝编译存在数据竞争的代码!
数据竞争会导致未定义行为,难以在运行时追踪并且难以诊断和修复Rust 避免了这种情况的发生因为直接拒绝编译存在数据竞争的代码!
一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能**同时**拥有:
一如既往,可以使用大括号来创建一个新的作用域来允许拥有多个可变引用,只是不能 **同时** 拥有:
```rust
let mut s = String::from("hello");
@ -168,7 +168,7 @@ let r3 = &mut s; // BIG PROBLEM
错误如下:
```
```text
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> borrow_thrice.rs:6:19
@ -182,13 +182,13 @@ immutable
| - immutable borrow ends here
```
哇哦!我们**也**不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个读取数据的人有能力影响其他人读取到的数据。
哇哦!我们 **也** 不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在它的眼皮底下值突然就被改变了!然而,多个不可变引用是没有问题的因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug在编译时而不是运行时并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。
即使这些错误有时是使人沮丧的。记住这是 Rust 编译器在提早指出一个潜在的 bug在编译时而不是运行时并明确告诉你问题在哪而不是任由你去追踪为何有时数据并不是你想象中的那样。
### 悬垂引用
在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个**悬垂指针***dangling pointer*),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
在存在指针的语言中,容易通过释放内存时保留指向它的指针而错误地生成一个 **悬垂指针***dangling pointer*),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
让我们尝试创建一个悬垂引用:
@ -208,7 +208,7 @@ fn dangle() -> &String {
这里是错误:
```
```text
error[E0106]: missing lifetime specifier
--> dangle.rs:5:16
|
@ -224,12 +224,12 @@ error: aborting due to previous error
错误信息引用了一个我们还未涉及到的功能:**生命周期***lifetimes*)。第十章会详细介绍生命周期。不过,如果你不理会生命周期的部分,错误信息确实包含了为什么代码是有问题的关键:
```
```text
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,9 +241,9 @@ fn dangle() -> &String { // dangle returns a reference to a String
// Danger!
```
因为`s`是在`dangle`创建的,当`dangle`的代码执行完毕后,`s`将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的`String`!这可不好。Rust 不会允许我们这么做的。
因为 `s` 是在 `dangle` 创建的,当 `dangle` 的代码执行完毕后,`s` 将被释放。不过我们尝试返回一个它的引用。这意味着这个引用会指向一个无效的 `String`!这可不对。Rust 不会允许我们这么做的。
这里的解决方法是直接返回`String`
这里的解决方法是直接返回 `String`
```rust
fn no_dangle() -> String {
@ -259,7 +259,7 @@ fn no_dangle() -> String {
简要的概括一下对引用的讨论:
1. 在任意给定时间,**只能**拥有如下中的一个:
1. 在任意给定时间,**只能** 拥有如下中的一个:
* 一个可变引用。
* 任意数量的不可变引用。
2. 引用必须总是有效的。

@ -2,7 +2,7 @@
> [ch04-03-slices.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch04-03-slices.md)
> <br>
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
> commit df9e3b922335ec2c76b6c1c4ede31b7742103c48
另一个没有所有权的数据类型是 *slice*。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
@ -14,7 +14,7 @@
fn first_word(s: &String) -> ?
```
`first_word`这个函数有一个参数`&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取**部分**字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:
`first_word` 这个函数有一个参数 `&String`。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 **部分** 字符串的办法。不过,我们可以返回单词结尾的索引。让我们试试如列表 4-10 所示的代码:
<span class="filename">Filename: src/main.rs</span>
@ -32,26 +32,25 @@ fn first_word(s: &String) -> usize {
}
```
<span class="caption">Listing 4-10: The `first_word` function that returns a
byte index value into the `String` parameter</span>
<span class="caption">列表 4-10`first_word` 函数返回 `String` 参数的一个字节索引值</span>
让我们将代码分解成小块。因为需要一个元素一个元素的检查`String`中的值是否是空格,需要用`as_bytes`方法将`String`转化为字节数组:
让我们将代码分解成小块。因为需要一个元素一个元素的检查 `String` 中的值是否是空格,需要用 `as_bytes` 方法将 `String` 转化为字节数组:
```rust,ignore
let bytes = s.as_bytes();
```
接下来,使用`iter`方法在字节数据上创建一个迭代器:
接下来,使用 `iter` 方法在字节数组上创建一个迭代器:
```rust,ignore
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' ' {
@ -61,9 +60,9 @@ for (i, &item) in bytes.iter().enumerate() {
s.len()
```
现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单单一个`usize`,不过它只在`&String`的上下文中才是一个有意义的数字。换句话说,因为它是一个与`String`相分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 `first_word`函数的程序:
现在有了一个找到字符串中第一个单词结尾索引的方法了,不过这有一个问题。我们返回了单独一个 `usize`,不过它只在 `&String` 的上下文中才是一个有意义的数字。换句话说,因为它是一个与 `String` 相分离的值,无法保证将来它仍然有效。考虑一下列表 4-11 中使用了列表 4-10 `first_word` 函数的程序:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
# fn first_word(s: &String) -> usize {
@ -90,24 +89,23 @@ fn main() {
}
```
<span class="caption">Listing 4-11: Storing the result from calling the
`first_word` function then changing the `String` contents</span>
<span class="caption">列表 4-11储存 `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。
### 字符串 slice
**字符串 slice***string slice*)是`String`中一部分值的引用,它看起来像这样:
**字符串 slice***string slice*)是 `String` 中一部分值的引用,它看起来像这样:
```rust
let s = String::from("hello world");
@ -116,18 +114,17 @@ let hello = &s[0..5];
let world = &s[6..11];
```
这类似于获取整个`String`的引用不过带有额外的`[0..5]`部分。不同于整个`String`的引用,这是一个包含`String`内部的一个位置和所需元素数量的引用。
这类似于获取整个 `String` 的引用不过带有额外的 `[0..5]` 部分。不同于整个 `String` 的引用,这是一个包含 `String` 内部的一个位置和所需元素数量的引用。`start..end` 语法代表一个以 `start` 开头并一直持续到但不包含 `end` 的 range。
我们使用一个 range `[starting_index..ending_index]`来创建 slice不过 slice 的数据结构实际上储存了开始位置和 slice 的长度。所以就`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` 第 6 个字节的指针和长度值 5 的 slice。
图 4-12 展示了一个图例
图 4-12 展示了一个图例
<img alt="world containing a pointer to the 6th byte of String s and a length 5" src="img/trpl04-06.svg" class="center" style="width: 50%;" />
<span class="caption">Figure 4-12: String slice referring to part of a
`String`</span>
<span class="caption">图 4-12引用了部分 `String` 的字符串 slice</span>
对于 Rust 的`..` range 语法如果想要从第一个索引0开始可以不写两个点号之前的值。换句话说如下两个语句是相同的
对于 Rust 的 `..` range 语法如果想要从第一个索引0开始可以不写两个点号之前的值。换句话说如下两个语句是相同的
```rust
let s = String::from("hello");
@ -136,7 +133,18 @@ let slice = &s[0..2];
let slice = &s[..2];
```
由此类推,如果 slice 包含`String`的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:
由此类推,如果 slice 包含 `String` 的最后一个字节,也可以舍弃尾部的数字。这意味着如下也是相同的:
```rust
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
```
也可以同时舍弃这两个值来获取一个整个字符串的 slice。所以如下亦是相同的
```rust
let s = String::from("hello");
@ -147,9 +155,9 @@ let slice = &s[0..len];
let slice = &s[..];
```
在记住所有这些知识后,让我们重写`first_word`来返回一个 slice。“字符串 slice”的签名写作`&str`
在记住所有这些知识后,让我们重写 `first_word` 来返回一个 slice。“字符串 slice” 类型签名写作 `&str`
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
fn first_word(s: &String) -> &str {
@ -167,7 +175,7 @@ fn first_word(s: &String) -> &str {
我们使用跟列表 4-10 相同的方式获取单词结尾的索引,通过寻找第一个出现的空格。当我们找到一个空格,我们返回一个索引,它使用字符串的开始和空格的索引来作为开始和结束的索引。
现在当调用`first_word`时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。
现在当调用 `first_word` 时,会返回一个单独的与底层数据相联系的值。这个值由一个 slice 开始位置的引用和 slice 中元素的数量组成。
`second_word`函数也可以改为返回一个 slice
@ -175,7 +183,7 @@ fn first_word(s: &String) -> &str {
fn second_word(s: &String) -> &str {
```
现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向`String`的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上时不正确的,不过却没有任何直观的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的`first_word`会抛出一个编译时错误:
现在我们有了一个不易混杂的直观的 API 了,因为编译器会确保指向 `String` 的引用保持有效。还记得列表 4-11 程序中,那个当我们获取第一个单词结尾的索引不过接着就清除了字符串所以索引就无效了的 bug 吗?那些代码逻辑上是不正确的,不过却没有表现出任何直接的错误。问题会在之后尝试对空字符串使用第一个单词的索引时出现。slice 就不可能出现这种 bug 并让我们更早的知道出问题了。使用 slice 版本的 `first_word` 会抛出一个编译时错误:
<span class="filename">Filename: src/main.rs</span>
@ -191,7 +199,7 @@ fn main() {
这里是编译错误:
```
```text
17:6 error: cannot borrow `s` as mutable because it is also borrowed as
immutable [E0502]
s.clear(); // Error!
@ -207,7 +215,7 @@ fn main() {
^
```
回忆一下借用规则,当拥有某值的不可变引用时。不能再获取一个可变引用。因为`clear`需要清空`String`它尝试获取一个可变引用它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整个错误类型
回忆一下借用规则,当拥有某值的不可变引用时,就不能再获取一个可变引用。因为 `clear` 需要清空 `String`它尝试获取一个可变引用它失败了。Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整类的错误
#### 字符串字面值就是 slice
@ -217,23 +225,23 @@ fn main() {
let s = "Hello, world!";
```
这里`s`的类型是`&str`:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的`&str`是一个不可变引用。
这里 `s` 的类型是 `&str`:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的`&str` 是一个不可变引用。
#### 字符串 slice 作为参数
在知道了能够获取字面值和`String`的 slice 后引起了另一个对`first_word`的改进,这是它的签名:
在知道了能够获取字面值和 `String` 的 slice 后引起了另一个对 `first_word` 的改进,这是它的签名:
```rust,ignore
fn first_word(s: &String) -> &str {
```
相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对`String`和`&str`使用相同的函数:
相反一个更有经验的 Rustacean 会写下如下这一行,因为它使得可以对 `String` `&str` 使用相同的函数:
```rust,ignore
fn first_word(s: &str) -> &str {
```
如果有一个字符串 slice可以直接传递它。如果有一个`String`,则可以传递整个`String`的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能:
如果有一个字符串 slice可以直接传递它。如果有一个 `String`,则可以传递整个 `String` 的 slice。定义一个获取字符串 slice 而不是字符串引用的函数使得我们的 API 更加通用并且不会丢失任何功能:
<span class="filename">Filename: src/main.rs</span>
@ -282,10 +290,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 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个`struct`中。
所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,这将贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个 `struct` 中。

@ -4,4 +4,4 @@
> <br>
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
`struct`,是 *structure* 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct`就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定**行为**的函数。`struct`和`enum`(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。
`struct`,是 *structure* 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct` 就像对象中的数据属性(字段)。在本章中,我们会比较元组与结构体,展示如何使用结构体,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。结构体和 **枚举***enum*(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查来在程序范围内创建新类型的基本组件。

Loading…
Cancel
Save