mirror of
https://github.com/gnu4cn/rust-lang-zh_CN.git
synced 2025-03-14 19:30:29 +08:00
Refining Ch04.
This commit is contained in:
parent
60bd1a1a61
commit
377ccdf205
@ -1,42 +1,6 @@
|
||||
fn main() {
|
||||
let s = String::from("The quick brown fox jumps over the lazy dog.");
|
||||
let s1 = String::from("hello");
|
||||
let s2 = s1;
|
||||
|
||||
// 函数 first_word 在 String 值的切片上有效,不管是部分还是全部的切片
|
||||
let word = first_word(&s[0..6]);
|
||||
println! ("{}", word);
|
||||
|
||||
let word = first_word(&s[..]);
|
||||
println! ("{}", word);
|
||||
|
||||
// 函数 first_word 还在 String 变量的引用上有效,而 String 变量的引用
|
||||
// 与 String 值的整个切片是等价的
|
||||
let word = first_word(&s);
|
||||
println! ("{}", word);
|
||||
|
||||
let s_string_literal = "hello word";
|
||||
|
||||
// 函数 first_word 在字符串字面值上有效,不论是部分还是整体
|
||||
let word = first_word(&s_string_literal[0..6]);
|
||||
println! ("{}", word);
|
||||
|
||||
let word = first_word(&s_string_literal[..]);
|
||||
println! ("{}", word);
|
||||
|
||||
// 由于字符串字面值已经 是 字符串切片,因此无需切片语法,这
|
||||
// 也是有效的!
|
||||
let word = first_word(s_string_literal);
|
||||
|
||||
println! ("{}", word);
|
||||
}
|
||||
|
||||
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[..]
|
||||
println! ("{}, world!", s1);
|
||||
}
|
||||
|
@ -16,15 +16,15 @@
|
||||
>
|
||||
> 许多编程语言,都不要求咱们经常考虑堆栈和堆。但是,在 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*。栈上存储的所有数据,必须有已知、固定的大小。编译时大小未知,或大小可能改变的数据,必须存储在堆上。
|
||||
>
|
||||
> 堆的组织程度则较低:在咱们把数据放在堆上时,咱们要请求一定数量的空间。内存分配器会在堆中,找到足够大的空位,将其标记为在用,并返回一个 *指针,pointer*,即那个位置的地址。这个过程称为 *在堆上分配,allocating on the heap*,有时也简称为 *分配,allocating*(将值推入栈,则不被视为分配)。由于到堆的指针,属于已知、固定的大小,因此咱们可以将该指针,存储在栈上,而当咱们需要具体数据时,就必须跟随这个指针。请设想在餐厅等待安排座位的情景。当咱们进入某家餐厅时,咱们要说明咱们团体的人数,然后接待员会找到一张适合每个人的空桌,并把咱们领到那里。如果咱们团队中有人来晚了,他们可以询问,咱们的座位在哪里,然后找到咱们。
|
||||
>
|
||||
> 压入栈要比在堆上分配空间更快,因为分配器无需寻找存储新数据的位置;该位置总是在栈的顶部。相比之下,在堆上分配空间,则需要更多的工作,因为分配器必须首先找到一个足够大的空间来存放数据,然后进行簿记,为下一次分配做好准备。
|
||||
>
|
||||
> 访问堆中的数据,比访问栈中的数据要慢,因为咱们必须跟随指针才能到达那里。如果减少在内存中的跳转,那么现代处理器的速度就会更快。继续类比,请设想一下某个餐厅的服务员,从许多桌子上点菜的情况。最有效的方法是先处理一张桌子上的所有点餐,然后再处理下一张桌子上的点餐。从 A 桌点菜,然后从 B 桌点菜,然后再从 A 桌点菜,然后再从 B 桌点菜,这个过程就会慢得多。同样,如果处理器处理的数据,与其他数据距离较近(如栈中的数据),而不是较远(如堆中的数据),那么处理器就能更好地完成工作。
|
||||
> 访问堆中的数据,比访问栈上的数据要慢,因为咱们必须跟随指针才能到达那里。如果减少在内存中的跳转,那么现代处理器的速度就会更快。继续类比,请设想一下某个餐厅的服务员,从许多桌子上点菜的情况。最有效的方法是先处理一张桌子上的所有点餐,然后再处理下一张桌子上的点餐。从 A 桌点菜,然后从 B 桌点菜,然后再从 A 桌点菜,然后再从 B 桌点菜,这个过程就会慢得多。同样,如果处理器处理的数据,与其他数据距离较近(如栈上的数据),而不是较远(如堆中的数据),那么处理器就能更好地完成工作。
|
||||
>
|
||||
> 当咱们的代码调用某个函数时,传入函数的值(可能包括指向堆上数据的指针)和函数的局部变量,会被推入栈。函数结束后,这些值会从栈中弹出。
|
||||
> 当咱们的代码调用某个函数时,传入函数的值(可能包括指向堆上数据的指针)和函数的局部变量,会被推入栈。函数结束后,这些值会从栈上弹出。
|
||||
>
|
||||
> 跟踪代码的哪些部分,正在使用堆上的哪些数据、尽量减少堆上的重复数据量,以及清理堆上未使用数据以免空间耗尽,这些都是所有权要解决的问题。一旦咱们掌握了所有权,咱们就不需要经常考虑栈和堆了,而清楚所有权的主要目的,是为管理堆数据这一点,有助于解释为什么他以这种方式工作。
|
||||
|
||||
@ -88,7 +88,7 @@ let s = "hello";
|
||||
**The `String` Type**
|
||||
|
||||
|
||||
为了说明所有权规则,我们需要一种比第 3 章 [数据类型](../programming_concepts/data_types.md) 小节中,介绍的数据类型更复杂的一种数据类型。前面介绍的类型大小已知,可被存储在栈中,并在其作用域结束时,从栈中弹出,如果代码的另一部分,需要在不同的作用域中使用同一个值,他们就可以快速、简便地复制,以创建一个新的、独立的实例。但我们打算看看存储在堆上的数据,并探讨 Rust 如何知道,何时清理这些数据,而 `String` 类型就是个很好的例子。
|
||||
为了说明所有权规则,我们需要一种比第 3 章 [数据类型](../programming_concepts/data_types.md) 小节中,介绍的数据类型更复杂的一种数据类型。前面介绍的类型大小已知,可被存储在栈上,并在其作用域结束时,从栈上弹出,如果代码的另一部分,需要在不同的作用域中使用同一个值,他们就可以快速、简便地复制,以创建一个新的、独立的实例。但我们打算看看存储在堆上的数据,并探讨 Rust 如何知道,何时清理这些数据,而 `String` 类型就是个很好的例子。
|
||||
|
||||
我们将重点关注 `String` 中,与所有权相关的部分。这些方面也适用于其他复杂的数据类型,无论它们是由标准库提供,还是由咱们自己创建。我们将在 [第 8 章](../common_collections/strings.md) 中,更深入地讨论 `String`。
|
||||
|
||||
@ -136,7 +136,7 @@ let s = String::from("hello");
|
||||
|
||||
不过,第二部分有所不同。在有 *垃圾回收器,garbage collector,GC* 的语言中,GC 会跟踪并清理不再使用的内存,我们不需要考虑这个问题。在大多数没有 GC 的语言中,我们有责任识别内存何时不再被使用,并调用代码显式释放内存,就像我们请求内存时一样。正确做到这一点,历来是编程中的难题。如果我们忘记了,就会浪费内存。如果过早释放,就会产生无效变量。如果我们做了两次,那也是一个错误。我们需要在 `allocate` 一次内存的同时,严格 `free` 一次内存。
|
||||
|
||||
Rust 采用了不同的路径:一旦拥有内存的变量超出作用域,该内存就会自动返回。下面是清单 4-1 中,咱们作用域示例的一个使用了 `String`,而非字符串字面值的版本:
|
||||
Rust 采取了不同路径:一旦拥有内存的变量超出作用域,该内存就会自动退回。下面是清单 4-1 中,咱们作用域示例的一个使用了 `String`,而非字符串字面值的版本:
|
||||
|
||||
|
||||
```rust
|
||||
@ -148,91 +148,107 @@ Rust 采用了不同的路径:一旦拥有内存的变量超出作用域,该
|
||||
// 不再有效
|
||||
```
|
||||
|
||||
其中就存在可将那个 `String` 类型的值所需的内存,退回给内存分配器的一个天然时间点:即在变量 `s` 超出作用域时。在变量超出作用域时,Rust 就会主动调用一个特殊函数。该函数名为 `drop`,其正是 `String` 类型的编写者,放置用于内存退回的代码之处。在那个结束花括号处,Rust 会自动调用这个 `drop` 函数。
|
||||
|
||||
> 注意:在 C++ 中,在某程序中项目生命周期结束时,资源重分配的这种模式,有时被称为 *资源获取即初始化*(in C++, this pattern of deallocating resources at the end of an item's lifetime is sometimes called *Resource Acquisition Is Initialization, RAII*)。若曾用过 RAII 模式,那么 Rust 中的这个 `drop` 函数就会不那么陌生了。
|
||||
|
||||
这种模式对 Rust 代码编写方式有深远影响。在此刻他可能看起来还算简单,但在想要让多个变量,使用早先在内存堆上分配的数据,这种更为复杂情形时,代码行为就会无法被预见到。现在就来探讨一下一些这样的情况。
|
||||
其中有个我们可将 `String` 所需的内存,归还给分配器的天然时间点:当 `s` 超出作用域时。当某个变量超出作用域时,Rust 会为我们,调用一个特殊函数。这个函数叫做 `drop`,这便是 `String` 的作者,可将返回内存代码放置的地方。Rust 会在那个结尾的大括号处,自动调用 `drop` 函数。
|
||||
|
||||
|
||||
## 变量与数据互操作方式之一:迁移(所有权)
|
||||
> **注意**:在 C++ 中,这种在某个项目生命周期结束时,解分配资源的模式,有时被称为 *资源获取即初始化,Resource Acquisition Is Initialization,RAII*。如果咱们使用过 RAII 模式,那么 Rust 中的 `drop` 函数对咱们一定不会陌生。
|
||||
|
||||
|
||||
这种模式对 Rust 代码的编写方式,影响深远。现在看来可能很简单,但在当我们打算让多个变量,使用我们在堆上分配的数据,这种更为复杂的情况下,代码的行为就会出乎意料。现在我们来探讨一下,其中的一些情况。
|
||||
|
||||
|
||||
## 变量与数据相互作用:迁移
|
||||
|
||||
**Variables and Data Interacting with Move**
|
||||
|
||||
|
||||
在 Rust 中,多个变量可以不同的方式,与同一数据交互。我们来看看清单 4-2 中,使用整数的示例。
|
||||
|
||||
在 Rust 中,多个变量可以多种方式,与同一数据进行互操作。来看看下面清单 4-2 中用到整数的示例:
|
||||
|
||||
```rust
|
||||
let x = 5;
|
||||
let y = x;
|
||||
```
|
||||
|
||||
*清单 4-2:将变量 `x` 的整数值,赋值给变量 `y`*
|
||||
|
||||
或许能猜到这段代码正在完成的事情:“把值 `5` 绑定到变量 `x`;随后构造一份 `x` 中值的拷贝并将其绑定到变量 `y`。” 现在就有了两个变量,`x` 与 `y`,且他们都等于 `5`。由于整数是有着已知的、固定大小的简单值,因此这实际上就是正在发生的事情,且这两个 `5` 的值都是被压入到栈上的。
|
||||
|
||||
> **注**:这就是下面会讲到的 [栈上数据的拷贝,copy](#唯栈数据拷贝stack-only-data-copy) 情形。
|
||||
*清单 4-2:将变量 `x` 的整数值,赋值给 `y`*
|
||||
|
||||
|
||||
那么现在来看看 `String` 的版本:
|
||||
我们大致可以猜到这是在做什么: “将值 `5` 绑定到 `x`;然后构造 `x` 中值的一份拷贝,并将其绑定到 `y`”。现在我们有两个变量,`x` 和 `y`,他们都等于 `5`。这确实是正在发生的事情,因为整数属于有着已知、固定大小的简单值,而这两个 `5` 值,都会被压入栈上。
|
||||
|
||||
|
||||
> **注**:这就是下面会讲到的 [唯栈数据:拷贝](#唯栈数据拷贝stack-only-data-copy) 情形。
|
||||
|
||||
|
||||
现在我们来看看 `String` 这个版本:
|
||||
|
||||
|
||||
```rust
|
||||
let s1 = String::from("hello");
|
||||
let s2 = s1;
|
||||
```
|
||||
|
||||
这代码看起来与上面的非常相似,那么这里就可以假定其工作方式也是一样的:那就是,第二行将构造出一个 `s1` 中值的拷贝,并将该拷贝绑定到 `s2`。不过这并非真的是实际发生的样子。
|
||||
这看起来非常相似,因此我们可能会认为,其工作方式是一样的:即第二行构造出一份 `s1` 中值的拷贝,并将其绑定到 `s2`。但事实并非如此。
|
||||
|
||||
> **注**:下面的代码将打印出 `s1 = 你好, s2 = 你好`,表示类型 `&str` (字符串切片)是存储在栈上的。
|
||||
|
||||
> **注**:下面的代码将打印出 `s1: 你好!, s2: 你好!`,表示类型 `&str` (字符串切片)是存储在栈上的。
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let s1 = "你好";
|
||||
let s1 = "你好!";
|
||||
let s2 = s1;
|
||||
|
||||
println! ("s1 = {}, s2 = {}", s1, s2);
|
||||
println! ("s1: {s1}, s2: {s2}");
|
||||
}
|
||||
```
|
||||
|
||||
请参阅下面的图 4-1,来搞明白在幕后 `String` 到底发生了什么。`String` 类型的值,是由三部分构成,在下图中的左边有给出:一个指向到保存该字符串内容内存的指针、一个长度,和一个该字符串的容量。这样一组数据被保存在栈上。下图的右边,即是内存堆上保存着字符串内容的内存。
|
||||
|
||||
请看图 4-1,了解 `String` 的到底发生了什么。一个 `String` 由三部分组成(如左图所示):指向存放该字符串内容的内存的一个指针、一个长度与一个容量。这组数据,是存储在栈上的。而图的右边,则是堆上存放着内容的内存。
|
||||
|
||||
|
||||

|
||||
|
||||
*图 4-1:、保存着绑定到变量 `s1` 的值 `hello` 的一个 `String` 类型值在内存中的表示*
|
||||
*图 4-1:保存绑定到 `s1` 的值 `"hello"` 的 `String` 在内存中的表示*
|
||||
|
||||
> **注**:`String` 类似属于 [灵巧指针,smart pointer](Ch15_Smart_Pointers.md),他是个包含了指针与其他一些元数据的,带有一些方法的特别能力的结构体。
|
||||
> **注**:`String` 类似属于 [灵巧指针](../Ch15_Smart_Pointers.md),他是个包含了指针与其他一些元数据,有着一些方法特别能力的结构体。
|
||||
|
||||
其中的长度,即为以字节计数、该 `String` 值内容正使用着的内存数量。而容量则是该 `String` 值从内存分配器处收到的、以字节计算的内存数量。长度与容量之间的区别,会相当重要,但在此情形下尚不重要,到目前未知,是可以忽略容量这个部分的。
|
||||
|
||||
在将 `s1` 赋值给 `s2` 时,这个 `String` 值被拷贝了,表示这里拷贝了栈上的指针、长度和容量。这里并未拷贝指针指向的、内存堆上的数据。也就是说,内存中数据的表示,如下图 4-2 所示:
|
||||
长度是这个 `String` 的内容当前所使用的内存容量(以字节为单位)。容量则是这个字符串,从内存分配器获得的内存总量(以字节为单位)。长度和容量之间的差异很重要,但在现在这个上下文中并不重要,所以忽略这个容量就可以了。
|
||||
|
||||
当我们把 `s1` 赋值给 `s2` 时,这个 `String` 数据会被复制,这意味着我们拷贝了栈上的指针、长度和容量。我们不会拷贝该指针所指向的堆上数据。换句话说,内存中的数据表示,会如下图 4-2 所示。
|
||||
|
||||
|
||||

|
||||
|
||||
*图 4-2:有着变量 `s1` 的指针、长度与容量拷贝的变量 `s2` 在内存中的表示*
|
||||
*图 4-2:变量 `s2` 在内存中的表示,其有着 `s1` 的指针、长度和容量的副本*
|
||||
|
||||
这种表示 *不* 同于下图 4-3,那才是 Rust 对内存堆上的数据进行拷贝时,内存看起来的样子。如果 Rust 像下图 4-3 中那样做,那么当内存堆上的数据较大时, `s2 = s1` 的这个操作,将会在运行时性能开销上代价高昂。
|
||||
|
||||
如果 Rust 也拷贝了堆数据,那么内存的表示就不会如图 4-3 所示。如果 Rust 这样做了,那么在堆上的数据很大时,操作 `s2 = s1` 在运行时的性能方面,可能会非常昂贵。
|
||||
|
||||

|
||||
|
||||
*图 4-3:`s2 = s1` 操作的另一种可能:Rust 同时拷贝内存堆数据*
|
||||
*图 4-3:如果 Rust 也复制了堆数据,则 `s2 = s1` 可能会执行的另一种可能性*
|
||||
|
||||
早先曾讲过,在变量超出作用域后,Rust 会自动调用那个 `drop` 函数,而清理掉那个变量的堆内存。但图 4-2 则给出了两个指针都指向同一位置的情况。这就是个问题了:在 `s2` 与 `s1` 都超出作用域时,他们都将尝试去释放那同样的内存。这被称为 *双重释放,double free* 错误,是先前提到过的内存安全错误之一,one of the memory safety bugs。二次释放内存,可导致内存损坏,而内存损坏则会潜在导致安全漏洞。
|
||||
|
||||
为确保内存安全,Rust 在代码行 `s2 = s1` 之后,便不再认为 `s1` 是有效的了。因此,在 `s1` 超出作用域后,Rust 便不需要释放任何内存。下面就来检查一下,在 `s2` 创建出来后,去尝试使用 `s1` 会发生什么;这样做是不会工作的:
|
||||
前面我们说过,当某个变量超出作用域时,Rust 会自动调用 `drop` 函数,并清理该变量的堆内存。但图 4-2 显示,两个数据指针都指向同一位置。这就有问题了:当 `s2` 和 `s1` 超出作用域时,他们都会尝试释放相同的内存。这就是所谓的 *双重释放,double free* 错误,也是我们之前提到的内存安全漏洞之一。释放两次内存,会导致内存损坏,从而可能导致安全漏洞。
|
||||
|
||||
为了确保内存安全,在 `let s2 = s1;` 之后,Rust 便将 `s1` 视为不再有效。因此,当 `s1` 离开作用域时,Rust 不需要释放任何东西。请在` s2` 创建出之后,看看尝试使用 `s1` 时会发生什么;他不会工作:
|
||||
|
||||
|
||||
```rust
|
||||
let s1 = String::from("hello"); // 这里 s 的类型为:String
|
||||
let s1 = String::from("hello");
|
||||
let s2 = s1;
|
||||
|
||||
println! ("{}", s1);
|
||||
println! ("{}, world!", s1);
|
||||
```
|
||||
|
||||
由于 Rust 阻止了对失效引用变量的使用,因此将收到一个下面这样的错误:
|
||||
|
||||
因为 Rust 阻止咱们使用无效引用,咱们就会得到一个下面这样的报错:
|
||||
|
||||
|
||||
```console
|
||||
$ cargo run
|
||||
Compiling string_demo v0.1.0 (/home/peng/rust-lang/projects/string_demo)
|
||||
Compiling ownership_demo v0.1.0 (/home/peng/rust-lang-zh_CN/projects/ownership_demo)
|
||||
warning: unused variable: `s2`
|
||||
--> src/main.rs:3:9
|
||||
|
|
||||
@ -242,21 +258,25 @@ warning: unused variable: `s2`
|
||||
= note: `#[warn(unused_variables)]` on by default
|
||||
|
||||
error[E0382]: borrow of moved value: `s1`
|
||||
--> src/main.rs:5:21
|
||||
--> src/main.rs:5:29
|
||||
|
|
||||
2 | let s1 = String::from("hello"); // 这里 s 的类型为:String
|
||||
2 | let s1 = String::from("hello");
|
||||
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
|
||||
3 | let s2 = s1;
|
||||
| -- value moved here
|
||||
4 |
|
||||
5 | println! ("{}", s1);
|
||||
| ^^ value borrowed here after move
|
||||
5 | println! ("{}, world!", s1);
|
||||
| ^^ value borrowed here after move
|
||||
|
|
||||
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
help: consider cloning the value if the performance cost is acceptable
|
||||
|
|
||||
3 | let s2 = s1.clone();
|
||||
| ++++++++
|
||||
|
||||
For more information about this error, try `rustc --explain E0382`.
|
||||
warning: `string_demo` (bin "string_demo") generated 1 warning
|
||||
error: could not compile `string_demo` due to previous error; 1 warning emitted
|
||||
warning: `ownership_demo` (bin "ownership_demo") generated 1 warning
|
||||
error: could not compile `ownership_demo` (bin "ownership_demo") due to previous error; 1 warning emitted
|
||||
```
|
||||
|
||||
若在使用其他编程语言时,曾听说过 *浅拷贝(shallow copy)* 和 *深拷贝(deep copy)* 这两个说法,那么这种对指针、长度与容量的拷贝,而未拷贝数据的概念,或许听起来像是进行了一次浅拷贝。但由于 Rust 还将第一个变量进行了失效处理,因此这里就不叫浅拷贝,而叫做 *迁移(move)*。在这个示例中,就会讲,变量 `s1` 已被 *迁移* 到变量 `s2` 里了。因此真实发生的事情,就是下图 4-4 显示的那样:
|
||||
|
Loading…
Reference in New Issue
Block a user