pull/864/head
kazeno 2 months ago
parent 705bbfeba4
commit 2a6921863c

@ -195,7 +195,7 @@ again!
#### 使用 `for` 遍历集合 #### 使用 `for` 遍历集合
可以使用 `while` 结构来遍历集合中的元素,比如数组。例如,看看示例 3-4。 可以使用 `while` 结构来遍历集合中的元素,比如数组。例如,示例 3-4 中的循环会打印数组 `a` 中的每一个元素
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -203,15 +203,15 @@ again!
{{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-04/src/main.rs}} {{#rustdoc_include ../listings/ch03-common-programming-concepts/listing-03-04/src/main.rs}}
``` ```
<span class="caption">示例 3-4使用 `while` 循环遍历集合中的元素</span> <span class="caption">示例 3-4使用 `while` 循环遍历集合中的每一个元素</span>
这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为)。运行这段代码会打印出数组中的每一个元素: 这里,代码对数组中的元素进行计数。它从索引 `0` 开始,并接着循环直到遇到数组的最后一个索引(这时,`index < 5` 不再为 `true`)。运行这段代码会打印出数组中的每一个元素:
```console ```console
{{#include ../listings/ch03-common-programming-concepts/listing-03-04/output.txt}} {{#include ../listings/ch03-common-programming-concepts/listing-03-04/output.txt}}
``` ```
数组中的所有五个元素都如期被打印出来。尽管 `index` 在某一时刻会到达值 `5`,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。 数组中的所有五个元素都如期出现在终端中。尽管 `index` 在某一时刻会到达值 `5`,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
但这个过程很容易出错;如果索引长度或测试条件不正确会导致程序 panic。例如如果将 `a` 数组的定义改为包含 4 个元素而忘记了更新条件 `while index < 4`,则代码会 panic。这也使程序更慢因为编译器增加了运行时代码来对每次循环进行条件检查以确定在循环的每次迭代中索引是否在数组的边界内。 但这个过程很容易出错;如果索引长度或测试条件不正确会导致程序 panic。例如如果将 `a` 数组的定义改为包含 4 个元素而忘记了更新条件 `while index < 4`,则代码会 panic。这也使程序更慢因为编译器增加了运行时代码来对每次循环进行条件检查以确定在循环的每次迭代中索引是否在数组的边界内。
@ -233,8 +233,6 @@ again!
下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range。 下面是一个使用 `for` 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,`rev`,用来反转 range。
注意:以下代码不会踏足到数字 4仅从一个数字开始到另一个数字之前。
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
```rust ```rust
@ -251,7 +249,7 @@ again!
* 生成第 n 个斐波那契数。 * 生成第 n 个斐波那契数。
* 打印圣诞颂歌 “The Twelve Days of Christmas” 的歌词,并利用歌曲中的重复部分(编写循环)。 * 打印圣诞颂歌 “The Twelve Days of Christmas” 的歌词,并利用歌曲中的重复部分(编写循环)。
当你准备好继续的时候,让我们讨论一个其他语言中 **并不** 常见的概念所有权ownership 当你准备好继续的时候,让我们讨论一个其他语言中**并不**常见的概念所有权ownership
[comparing-the-guess-to-the-secret-number]: [comparing-the-guess-to-the-secret-number]:
ch02-00-guessing-game-tutorial.html#比较猜测的数字和秘密数字 ch02-00-guessing-game-tutorial.html#比较猜测的数字和秘密数字

@ -1,7 +1,6 @@
# 认识所有权 # 认识所有权
> [ch04-00-understanding-ownership.md](https://github.com/rust-lang/book/blob/main/src/ch04-00-understanding-ownership.md) <!-- https://github.com/rust-lang/book/blob/main/src/ch04-00-understanding-ownership.md -->
> <br> <!-- commit a5e0c5b2c5f9054be3b961aea2c7edfeea591de8 -->
> commit a5e0c5b2c5f9054be3b961aea2c7edfeea591de8
所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收garbage collector即可保障内存安全因此理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能借用borrowing、slice 以及 Rust 如何在内存中布局数据。 所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收garbage collector即可保障内存安全因此理解 Rust 中所有权如何工作是十分重要的。本章我们将讲到所有权以及相关功能借用borrowing、slice 以及 Rust 如何在内存中布局数据。

@ -1,10 +1,9 @@
## 什么是所有权? ## 什么是所有权?
> [ch04-01-what-is-ownership.md](https://github.com/rust-lang/book/blob/main/src/ch04-01-what-is-ownership.md) <!-- https://github.com/rust-lang/book/blob/main/src/ch04-01-what-is-ownership.md -->
> <br> <!-- commit f8ed2ced5daaa26e2b3a69df4bf5e1ce04dda758 -->
> commit 3d51f70c78162faaebcab0da0de2ddd333e7a8ed
**所有权***ownership*)是 Rust 用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制在程序运行时有规律地寻找不再使用的内存在另一些语言中程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。 **所有权***ownership*)是 Rust 用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制在程序运行时有规律地寻找不再使用的内存在另一些语言中程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序的运行
因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒! 因为所有权对很多程序员来说都是一个新概念,需要一些时间来适应。好消息是随着你对 Rust 和所有权系统的规则越来越有经验,你就越能自然地编写出安全和高效的代码。持之以恒!
@ -14,12 +13,13 @@
> >
> 在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。我们会在本章的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热的简要解释。 > 在很多语言中,你并不需要经常考虑到栈与堆。不过在像 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*)。栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。
>
> 堆是缺乏组织的当向堆放入数据时你要请求一定大小的空间。内存分配器memory allocator在堆的某处找到一块足够大的空位把它标记为已使用并返回一个表示该位置地址的 **指针***pointer*)。这个过程称作 **在堆上分配内存***allocating on the heap*),有时简称为 “分配”allocating将数据推入栈中并不被认为是分配。因为指向放入堆中数据的指针是已知的并且大小是固定的你可以将该指针存储在栈上不过当需要实际数据时必须访问指针。想象一下去餐馆就座吃饭。当进入时你说明有几个人餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了他们也可以通过询问来找到你们坐在哪。 > 堆是缺乏组织的当向堆放入数据时你要请求一定大小的空间。内存分配器memory allocator在堆的某处找到一块足够大的空位把它标记为已使用并返回一个表示该位置地址的 **指针***pointer*)。这个过程称作 **在堆上分配内存***allocating on the heap*),有时简称为 “分配”allocating将数据推入栈中并不被认为是分配。因为指向放入堆中数据的指针是已知的并且大小是固定的你可以将该指针存储在栈上不过当需要实际数据时必须访问指针。想象一下去餐馆就座吃饭。当进入时你说明有几个人餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了他们也可以通过询问来找到你们坐在哪。
> >
> 入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。 > 入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
> >
> 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作 > 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)更高效
> >
> 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。 > 当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
> >
@ -31,7 +31,7 @@
> 1. Rust 中的每一个值都有一个 **所有者***owner*)。 > 1. Rust 中的每一个值都有一个 **所有者***owner*)。
> 2. 值在任一时刻有且只有一个所有者。 > 2. 值在任一时刻有且只有一个所有者。
> 3. 当所有者(变量)离开作用域,这个值将被丢弃。 > 3. 当所有者离开作用域,这个值将被丢弃。
### 变量作用域 ### 变量作用域
@ -43,7 +43,7 @@
let s = "hello"; let s = "hello";
``` ```
变量 `s` 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 **作用域** 结束时都是有效的。示例 4-1 中的注释标明了变量 `s` 在何处是有效的。 变量 `s` 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前**作用域**结束时都是有效的。示例 4-1 中的注释标明了变量 `s` 在何处是有效的。
```rust ```rust
{{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-01/src/main.rs:here}} {{#rustdoc_include ../listings/ch04-understanding-ownership/listing-04-01/src/main.rs:here}}
@ -53,27 +53,21 @@ let s = "hello";
换句话说,这里有两个重要的时间点: 换句话说,这里有两个重要的时间点:
* 当 `s` **进入作用域** 时,它就是有效的。 * 当 `s` **进入作用域**时,它就是有效的。
* 这一直持续到它 **离开作用域** 为止。 * 这一直持续到它**离开作用域**为止。
目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍 `String` 类型。 目前为止,变量是否有效与作用域的关系跟其他编程语言是类似的。现在我们在此基础上介绍 `String` 类型。
### `String` 类型 ### `String` 类型
为了演示所有权的规则,我们需要一个比第三章 [“数据类型”][data-types] 中讲到的都要复杂的数据类型。前面介绍的类型都是已知大小的,可以存储在栈中,并且当离开作用域时被移出栈,如果代码的另一部分需要在不同的作用域中使用相同的值,可以快速简单地复制它们来创建一个新的独立实例。不过我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的。 为了演示所有权的规则,我们需要一个比第三章 [“数据类型”][data-types] 中讲到的都要复杂的数据类型。前面介绍的类型都是已知大小的,可以存储在栈中,并且当离开作用域时被移出栈,如果代码的另一部分需要在不同的作用域中使用相同的值,可以快速简单地复制它们来创建一个新的独立实例。不过我们需要寻找一个存储在堆上的数据来探索 Rust 是如何知道该在何时清理数据的,而 `String` 类型就是一个很好的例子
我们会专注于 `String` 与所有权相关的部分。这些方面也同样适用于标准库提供的或你自己创建的其他复杂数据类型。在[第八章][ch8]会更深入地讲解 `String` 我们会专注于 `String` 与所有权相关的部分。这些方面也同样适用于标准库提供的或你自己创建的其他复杂数据类型。在[第八章][ch8]会更深入地讲解 `String`
我们已经见过字符串字面值即被硬编码进程序里的字符串值。字符串字面值是很方便的不过它们并不适合使用文本的每一种场景。原因之一就是它们是不可变的。另一个原因是并非所有字符串的值都能在编写代码时就知道例如要是想获取用户输入并存储该怎么办呢为此Rust 有另一种字符串类型,`String`。这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 `from` 函数基于字符串字面值来创建 `String`,如下: 我们已经见过字符串字面值即被硬编码进程序里的字符串值。字符串字面值是很方便的不过它们并不适合使用文本的每一种场景。原因之一就是它们是不可变的。另一个原因是并非所有字符串的值都能在编写代码时就知道例如要是想获取用户输入并存储该怎么办呢为此Rust 有另一种字符串类型,`String`。这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 `from` 函数基于字符串字面值来创建 `String`,如下:
```rust
let s = String::from("hello");
```
这两个冒号 `::` 是运算符,允许将特定的 `from` 函数置于 `String` 类型的命名空间namespace而不需要使用类似 `string_from` 这样的名字。在第五章的 [“方法语法”“Method Syntax”][method-syntax] 部分会着重讲解这个语法,而且在第七章的 [“路径用于引用模块树中的项”][paths-module-tree] 中会讲到模块的命名空间。 这两个冒号 `::` 是运算符,允许将特定的 `from` 函数置于 `String` 类型的命名空间namespace而不需要使用类似 `string_from` 这样的名字。在第五章的 [“方法语法”“Method Syntax”][method-syntax] 部分会着重讲解这个语法,而且在第七章的 [“路径用于引用模块树中的项”][paths-module-tree] 中会讲到模块的命名空间。
**可以** 修改此类字符串:
```rust ```rust
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-01-can-mutate-string/src/main.rs:here}} {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-01-can-mutate-string/src/main.rs:here}}
``` ```
@ -105,9 +99,7 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
这个模式对编写 Rust 代码的方式有着深远的影响。现在它看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。 这个模式对编写 Rust 代码的方式有着深远的影响。现在它看起来很简单,不过在更复杂的场景下代码的行为可能是不可预测的,比如当有多个变量使用在堆上分配的内存时。现在让我们探索一些这样的场景。
<a id="ways-variables-and-data-interact-move"></a> #### 使用移动的变量与数据交互
#### 变量与数据交互的方式(一):移动
在 Rust 中,多个变量可以采取不同的方式与同一数据进行交互。让我们看看示例 4-2 中一个使用整型的例子。 在 Rust 中,多个变量可以采取不同的方式与同一数据进行交互。让我们看看示例 4-2 中一个使用整型的例子。
@ -117,7 +109,7 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
<span class="caption">示例 4-2将变量 `x` 的整数值赋给 `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` 版本:
@ -125,7 +117,7 @@ Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-03-string-move/src/main.rs:here}} {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-03-string-move/src/main.rs:here}}
``` ```
这看起来与上面的代码非常类似,所以我们可能会假设它们的运行方式也是类似的:也就是说,第二行可能会生成一个 `s1` 的拷贝并绑定到 `s2` 上。不过,事实上并不完全是这样 这看起来与上面的代码非常类似,所以我们可能会假设它们的运行方式也是类似的:也就是说,第二行可能会生成一个 `s1` 的拷贝并绑定到 `s2` 上。但事实并非如此
看看图 4-1 以了解 `String` 的底层会发生什么。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。 看看图 4-1 以了解 `String` 的底层会发生什么。`String` 由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
@ -147,7 +139,7 @@ src="img/trpl04-02.svg" class="center" style="width: 50%;" />
<span class="caption">图 4-2变量 `s2` 的内存表现,它有一份 `s1` 指针、长度和容量的拷贝</span> <span class="caption">图 4-2变量 `s2` 的内存表现,它有一份 `s1` 指针、长度和容量的拷贝</span>
这个表现形式看起来 **并不像** 图 4-3 中的那样,如果 Rust 也拷贝了堆上的数据,那么内存看起来就是这样的。如果 Rust 这么做了,那么操作 `s2 = s1` 在堆上数据比较大的时候会对运行时性能造成非常大的影响。 这个表现形式看起来**并不像**图 4-3 中的那样,如果 Rust 也拷贝了堆上的数据,那么内存看起来就是这样的。如果 Rust 这么做了,那么操作 `s2 = s1` 在堆上数据比较大的时候会对运行时性能造成非常大的影响。
<img alt="Four tables: two tables representing the stack data for s1 and s2, <img alt="Four tables: two tables representing the stack data for s1 and s2,
and each points to its own copy of string data on the heap." and each points to its own copy of string data on the heap."
@ -181,9 +173,15 @@ access the heap data." src="img/trpl04-04.svg" class="center" style="width:
这样就解决了我们的问题!因为只有 `s2` 是有效的,当其离开作用域,它就释放自己的内存,完毕。 这样就解决了我们的问题!因为只有 `s2` 是有效的,当其离开作用域,它就释放自己的内存,完毕。
另外这里还隐含了一个设计选择Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 **自动** 的复制都可以被认为是对运行时性能影响较小的。 另外这里还隐含了一个设计选择Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何**自动**的复制都可以被认为是对运行时性能影响较小的。
#### 作用域与赋值
作用域、所有权和内存的关系反过来也是对的,它们也会被 `drop` 函数释放。当你给一个已有的变量赋一个全新的值时Rust 将会立即调用 `drop` 并释放原始值的内存。例如,考虑如下代码:
<a id="ways-variables-and-data-interact-clone"></a> ```rust
{{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-04b-replacement-drop/src/main.rs:here}}
```
#### 变量与数据交互的方式(二):克隆 #### 变量与数据交互的方式(二):克隆

Loading…
Cancel
Save