Updated src/ownership/about_ownership.md'.

This commit is contained in:
Hector PENG
2026-03-09 15:54:32 +08:00
parent ed128e1302
commit f2a7ac6ced
9 changed files with 271 additions and 151 deletions

View File

@@ -1,7 +1,13 @@
fn main() {
let mut s = String::from("hello");
let s1 = String::from("hello");
s.push_str(", world!"); // push_str() 方法会追加一个字面值,到某个 String
let (s2, len) = calculate_length(s1);
println! ("{}", s); // 这将打印出 `hello, world!`
println! ("字符串 '{s2}' 的长度为 {len}");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}

View File

@@ -1,7 +1,5 @@
# 编写猜数游戏
**Programming a Guessing Game**
咱们来一起完成一个实践项目,开始学习 Rust这一章通过向咱们展示如何在实际程序中如何运用他们向咱们介绍一些常见 Rust 概念。咱们将了解
@@ -80,6 +78,7 @@ Hello, world!
这个猜数游戏程序的第一部分,将请求用户输入,处理该输入并检查输入是否为预期形式。首先,我们将允许玩家输入一个猜测。请将清单 2-1 中的代码,输入到 `src/main.rs` 中。
<a name="#listing_2-1"></a>
文件名:`src/main.rs`
```rust
@@ -100,8 +99,7 @@ fn main() {
}
```
<a name="#listing_2-1"></a>
*清单 2-1从用户处获取一个猜数并将其打印出来的代码*
**清单 2-1**,从用户处获取一个猜数并将其打印出来的代码
这段代码包含了大量信息,所以我们来逐行查看。要获取用户输入,然后将结果作为输出打印,我们就需要将 `io` 这个输入/输出库,带入作用域。`io` 库来自标准库,称为 `std`
@@ -313,6 +311,7 @@ Cargo 认为这些版本有着与 `0.8.5` 版本兼容的公共 API而这一
现在,在不修改任何代码下,我们来构建这个项目,如清单 2-2 中所示。
<a name="#listing_2-2"></a>
```console
$ cargo build
Updating `ustc` index
@@ -360,8 +359,7 @@ remote: Total 1043606 (delta 693593), reused 848030 (delta 620187), pack-reused
Finished `dev` profile [unoptimized + debuginfo] target(s) in 9m 51s
```
<a name="#listing_2-2"></a>
*清单 2-2在添加 `rand` 代码箱作为依赖项添加后,运行 `cargo build` 的输出**
**清单 2-2**:在添加 `rand` 代码箱作为依赖项添加后,运行 `cargo build` 的输出
> **译注**:这里译者使用了国内的 `ustc` Cargo 登记簿镜像。
@@ -430,6 +428,7 @@ rand = "0.999.0"
咱们来开始使用 `rand` 生成一个要猜的数字。下一步是要更新 `src/main.rs`,如下清单 2-3 中所示。
<a name="#listing_2-3"></a>
文件名:`src/main.rs`
```rust
@@ -455,8 +454,7 @@ fn main() {
}
```
<a name="#listing_2-3"></a>
*清单 2-3添加生成随机数的代码*
**清单 2-3**:添加生成随机数的代码
首先,我们要添加行 `use rand::Rng;``Rng` 这个特质定义了随机数生成器所实现的一些方法,而这个特质必须要在咱们要用到那些方法的作用域中。[第 10 章](./generic_types_traits_and_lifetimes/traits.md) 将详细介绍特质。
@@ -502,6 +500,7 @@ $ cargo run
现在我们有了用户输入和随机数,我们可以比较他们。该步骤于清单 2-4 中所示。请注意,这段代码还不会编译,我们将进行解释。
<a name="#listing_2-4"></a>
文件名:`src/main.rs`
```rust
@@ -521,8 +520,8 @@ fn main() {
}
}
```
<a name="#listing_2-4"></a>
*清单 2-4处理比较两个数字可能的返回值*
**清单 2-4**:处理比较两个数字可能的返回值
首先,我们添加另一条 `use` 语句,将一个名为 `std::cmp::Ordering` 的类型,从标准库中带入作用域。`Ordering` 类型属于另一个枚举,有着 `Less``Greater``Equal` 三种变体。这正是咱们在比较两个值时,可能的三种结果。
@@ -737,6 +736,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
为进一步完善游戏的行为,我们来将游戏构造为忽略非数字,以便用户可以继续猜数,而不是在用户输入非数字时崩溃程序。通过修改其中 `guess` 从字符串转换为 `u32` 的行,咱们便可做到这点,如下清单 2-5 中所示。
<a name="listing_2-5"></a>
文件名:`src/main.rs`
```rust
@@ -756,8 +756,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
// --跳过--
```
<a name="listing_2-5"></a>
*清单 2-5忽略非数字的猜数并请求另一猜数而不是让崩溃程序*
**清单 2-5**:忽略非数字的猜数并请求另一猜数,而不是让崩溃程序
我们从 `expect` 调用切换到 `match` 表达式,以从出错时崩溃程序转而处理错误。请记住,`parse` 会返回一个 `Result` 类型,而 `Result` 是个枚举,有着 `Ok``Err` 两个变种。我们在这里使用了个 `match` 表达式,就像我对 `cmp` 方法的 `Ordering` 结果所做的一样。
@@ -788,6 +787,7 @@ adb
太棒了!只需最后一个微小调整,我们即将完成这个猜数游戏了。回想一下这个程序仍在打印秘密数字。这对测试来说效果很好,但却毁掉了这个游戏。咱们来删除那个输出秘密数字的 `println!`。清单 2-6 给出了最终代码。
<a name="#listing_2-6"></a>
文件名:`src/main.rs`
```rust
@@ -829,8 +829,7 @@ fn main() {
}
```
<a name="#listing_2-6"></a>
*清单 2-6完整的猜数游戏代码*
**清单 2-6**:完整的猜数游戏代码
至此,咱们已成功构建了这个猜数游戏。恭喜!

View File

@@ -35,6 +35,7 @@ $ cd hello_cargo
在咱们所选的文本编辑器中,打开 `Cargo.toml`。他应该类似于清单 1-2 中的代码。
<a name="listing_1-2"></a>
文件名:`Cargo.toml`
```toml
@@ -46,7 +47,7 @@ edition = "2024"
[dependencies]
```
*清单 1-2由 `cargo new` 生成的 `Cargo.toml` 内容*
**清单 1-2**:由 `cargo new` 生成的 `Cargo.toml` 内容
这个文件属于 [TOML](https://toml.io/) *Tom's Obvious, Minimal Language* )格式,是 Cargo 的配置格式。
@@ -92,8 +93,6 @@ $ cargo init
## 构建和运行 Cargo 项目
**Building and Running a Cargo Project**
现在,咱们来看看,使用 Cargo 构建和运行 "Hello, world!" 程序时,有哪些不同!请在 `hello_cargo` 目录下,输入以下命令来构建项目:
```console
@@ -113,6 +112,7 @@ Hello, world!
如果一切顺利,`Hello, world!` 就会打印到终端。首次运行 `cargo build`,还会导致 Cargo 在目录顶层,创建出一个新文件:`Cargo.lock`。该文件会跟踪项目中依赖项的确切版本。这个项目没有依赖项因此该文件有点稀疏。咱们永远都无需手动修改这个文件Cargo 会帮咱们管理其内容。
<a name="listing_1-3"></a>
文件名:`Cargo.lock`
```toml
@@ -125,7 +125,7 @@ name = "hello_cargo"
version = "0.1.0"
```
*清单 1-3, `Cargo.lock`*
**清单 1-3**, `Cargo.lock`
咱们刚刚使用 `cargo build` 构建了个项目,并使用 `./target/debug/hello_cargo` 运行了他,但我们也可以使用 `cargo run`,在一条命令中,编译代码并随后运行得到的可执行文件:

View File

@@ -39,6 +39,7 @@ $ cd hello_world
现在打开咱们刚创建的 `main.rs` 文件,输入下面清单 1-1 中的代码。
<a name="listing_1-1"></a>
文件名:`main.rs`
```rust
@@ -47,7 +48,7 @@ fn main() {
}
```
*清单 1-1打印 `Hello, World!` 的一个程序*
**清单 1-1**:打印 `Hello, World!` 的一个程序
请保存该文件,并返回到 `~/projects/hello_world` 目录下,咱们的终端窗口。在 Linux 或 macOS 上输入以下命令,编译并运行该文件:

95
src/images/trpl04-05.svg Normal file
View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 12.1.2 (20240928.0832)
-->
<!-- Pages: 1 -->
<svg
viewBox="0.00 0.00 1038.00 1342.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(4.16667 4.16667) rotate(0) translate(4 318)">
<polygon fill="white" stroke="none" points="-4,4 -4,-318 245,-318 245,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_heap</title>
</g>
<!-- s -->
<g id="node1" class="node">
<title>s</title>
<polyline fill="none" stroke="black" points="8,-288 94,-288"/>
<text text-anchor="start" x="48.38" y="-293.7" font-family="Times,serif" font-size="14.00">s</text>
<polygon fill="none" stroke="black" points="8,-266 8,-288 59,-288 59,-266 8,-266"/>
<text text-anchor="start" x="18.5" y="-271.7" font-family="Times,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="59,-266 59,-288 94,-288 94,-266 59,-266"/>
<text text-anchor="start" x="61.88" y="-271.7" font-family="Times,serif" font-size="14.00">value</text>
<polygon fill="none" stroke="black" points="8,-244 8,-266 59,-266 59,-244 8,-244"/>
<text text-anchor="start" x="26" y="-249.7" font-family="Times,serif" font-size="14.00">ptr</text>
<polygon fill="none" stroke="black" points="59,-244 59,-266 94,-266 94,-244 59,-244"/>
<polygon fill="none" stroke="black" points="8,-222 8,-244 59,-244 59,-222 8,-222"/>
<text text-anchor="start" x="25.25" y="-227.7" font-family="Times,serif" font-size="14.00">len</text>
<polygon fill="none" stroke="black" points="59,-222 59,-244 94,-244 94,-222 59,-222"/>
<text text-anchor="start" x="73.12" y="-227.7" font-family="Times,serif" font-size="14.00">4</text>
<polygon fill="none" stroke="black" points="8,-200 8,-222 59,-222 59,-200 8,-200"/>
<text text-anchor="start" x="11" y="-205.7" font-family="Times,serif" font-size="14.00">capacity</text>
<polygon fill="none" stroke="black" points="59,-200 59,-222 94,-222 94,-200 59,-200"/>
<text text-anchor="start" x="73.12" y="-205.7" font-family="Times,serif" font-size="14.00">4</text>
</g>
<!-- ahoy -->
<g id="node3" class="node">
<title>ahoy</title>
<polygon fill="none" stroke="black" points="146,-266 146,-288 182,-288 182,-266 146,-266"/>
<text text-anchor="start" x="149" y="-271.7" font-family="Times,serif" font-size="14.00">index</text>
<polygon fill="none" stroke="black" points="182,-266 182,-288 217,-288 217,-266 182,-266"/>
<text text-anchor="start" x="184.88" y="-271.7" font-family="Times,serif" font-size="14.00">value</text>
<polygon fill="none" stroke="black" points="146,-244 146,-266 182,-266 182,-244 146,-244"/>
<text text-anchor="start" x="160.62" y="-249.7" font-family="Times,serif" font-size="14.00">0</text>
<polygon fill="none" stroke="black" points="182,-244 182,-266 217,-266 217,-244 182,-244"/>
<text text-anchor="start" x="196.5" y="-249.7" font-family="Times,serif" font-size="14.00">a</text>
<polygon fill="none" stroke="black" points="146,-222 146,-244 182,-244 182,-222 146,-222"/>
<text text-anchor="start" x="160.62" y="-227.7" font-family="Times,serif" font-size="14.00">1</text>
<polygon fill="none" stroke="black" points="182,-222 182,-244 217,-244 217,-222 182,-222"/>
<text text-anchor="start" x="196.12" y="-227.7" font-family="Times,serif" font-size="14.00">h</text>
<polygon fill="none" stroke="black" points="146,-200 146,-222 182,-222 182,-200 146,-200"/>
<text text-anchor="start" x="160.62" y="-205.7" font-family="Times,serif" font-size="14.00">2</text>
<polygon fill="none" stroke="black" points="182,-200 182,-222 217,-222 217,-200 182,-200"/>
<text text-anchor="start" x="196.12" y="-205.7" font-family="Times,serif" font-size="14.00">o</text>
<polygon fill="none" stroke="black" points="146,-178 146,-200 182,-200 182,-178 146,-178"/>
<text text-anchor="start" x="160.62" y="-183.7" font-family="Times,serif" font-size="14.00">3</text>
<polygon fill="none" stroke="black" points="182,-178 182,-200 217,-200 217,-178 182,-178"/>
<text text-anchor="start" x="196.12" y="-183.7" font-family="Times,serif" font-size="14.00">y</text>
</g>
<!-- s&#45;&gt;ahoy -->
<g id="edge1" class="edge">
<title>s:c&#45;&gt;ahoy:pointee</title>
<path fill="none" stroke="black" d="M77.5,-255C77.5,-255 106.47,-255 133.85,-255"/>
<polygon fill="black" stroke="black" points="133.49,-258.5 143.49,-255 133.49,-251.5 133.49,-258.5"/>
</g>
<!-- hello -->
<g id="node2" class="node">
<title>hello</title>
<polygon fill="gray" stroke="none" points="146,-20 146,-152 217,-152 217,-20 146,-20"/>
<polygon fill="none" stroke="black" points="146,-130 146,-152 182,-152 182,-130 146,-130"/>
<text text-anchor="start" x="149" y="-135.7" font-family="Times,serif" font-size="14.00">index</text>
<polygon fill="none" stroke="black" points="182,-130 182,-152 217,-152 217,-130 182,-130"/>
<text text-anchor="start" x="184.88" y="-135.7" font-family="Times,serif" font-size="14.00">value</text>
<polygon fill="none" stroke="black" points="146,-108 146,-130 182,-130 182,-108 146,-108"/>
<text text-anchor="start" x="160.62" y="-113.7" font-family="Times,serif" font-size="14.00">0</text>
<polygon fill="none" stroke="black" points="182,-108 182,-130 217,-130 217,-108 182,-108"/>
<text text-anchor="start" x="196.12" y="-113.7" font-family="Times,serif" font-size="14.00">h</text>
<polygon fill="none" stroke="black" points="146,-86 146,-108 182,-108 182,-86 146,-86"/>
<text text-anchor="start" x="160.62" y="-91.7" font-family="Times,serif" font-size="14.00">1</text>
<polygon fill="none" stroke="black" points="182,-86 182,-108 217,-108 217,-86 182,-86"/>
<text text-anchor="start" x="196.5" y="-91.7" font-family="Times,serif" font-size="14.00">e</text>
<polygon fill="none" stroke="black" points="146,-64 146,-86 182,-86 182,-64 146,-64"/>
<text text-anchor="start" x="160.62" y="-69.7" font-family="Times,serif" font-size="14.00">2</text>
<polygon fill="none" stroke="black" points="182,-64 182,-86 217,-86 217,-64 182,-64"/>
<text text-anchor="start" x="197.62" y="-69.7" font-family="Times,serif" font-size="14.00">l</text>
<polygon fill="none" stroke="black" points="146,-42 146,-64 182,-64 182,-42 146,-42"/>
<text text-anchor="start" x="160.62" y="-47.7" font-family="Times,serif" font-size="14.00">3</text>
<polygon fill="none" stroke="black" points="182,-42 182,-64 217,-64 217,-42 182,-42"/>
<text text-anchor="start" x="197.62" y="-47.7" font-family="Times,serif" font-size="14.00">l</text>
<polygon fill="none" stroke="black" points="146,-20 146,-42 182,-42 182,-20 146,-20"/>
<text text-anchor="start" x="160.62" y="-25.7" font-family="Times,serif" font-size="14.00">4</text>
<polygon fill="none" stroke="black" points="182,-20 182,-42 217,-42 217,-20 182,-20"/>
<text text-anchor="start" x="196.12" y="-25.7" font-family="Times,serif" font-size="14.00">o</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -35,7 +35,7 @@
## 变量作用域
既然我们已经掌握了基本的 Rust 语法,我们将不在示例中包含所有 `fn main() {` 代码,所以若咱们在跟进学习,请务必手动将以下示例放入 `main` 函数中。如此,我们的示例将更加简洁,让我们专注于具体细节而不是样板代码了。
既然我们已经掌握了基本的 Rust 语法,我们将不在示例中包含所有 `fn main() {` 代码,所以若咱们在跟进学习,那么请务必手动将以下示例放入 `main` 函数中。如此,我们的示例将更加简洁,让我们专注于具体细节而不是样板代码了。
作为所有权的首个示例,我们将探讨一些变量的作用域。所谓 *作用域scope*,是程序中某个项目有效的范围。请看以下这个变量:
@@ -56,7 +56,7 @@
} // 此时该作用域结束,而变量 s 不再有效
```
*清单 4-1变量及其有效的作用域*
**清单 4-1**:变量及其有效的作用域
换句话说,这里有两个重要的时间点:
@@ -115,51 +115,48 @@ let s = String::from("hello");
然而,第二部分则有所不同。在带有 *垃圾回收器garbage collectorGC* 的语言中GC 会跟踪并清理不再使用的内存,我们不需要考虑他。在大多数没有 GC 的语言中,我们有责任识别内存何时不再被使用,并调用代码显式释放他,就像我们请求他一样。正确做到这一点,历来是编程难题。当我们忘记了了,我们将浪费内存。当我们过早进行时,我们将有着无效变量。当我们做了两次时,那也是个 bug。我们需要准确地将一次 `allocate` 与一次 `free` 配对。
Rust 采取了不同路径:一旦拥有内存的变量超出作用域,内存就会自动归还。下面是清单 4-1 中咱们作用域示例的一个版本,使用 `String` 而非字符串字面值:
Rust 采取了不同路径:一旦拥有内存的变量超出作用域,内存就会自动归还。下面是清单 4-1 中咱们作用域示例的一个版本,使用 `String` 而非字符串字面值:
```rust
{
let s = String::from("hello"); // 变量 s 自此往下是有效的
// 以变量 s 完成一些操作
// s 进行一些操作
} // 该作用域到此时结束,而变量 s
// 不再有效
```
其中有个我们可将 `String` 所需的内存归还给分配器的天然时间点:当 `s` 超出作用域时。当某个变量超出作用域时Rust 会为我们调用一个特殊函数。这个函数叫做 `drop`便是 `String` 作者,可将返回内存代码放置的地方。Rust 会在那个结尾的大括号处自动调用 `drop` 函数。
其中有个我们可将 `String` 所需的内存归还给分配器的天然时间点:当 `s` 超出作用域时。当变量超出作用域时Rust 会为我们调用一个特殊函数。这个函数叫做 `drop`便是 `String` 作者可放置归还内存代码之处。Rust 会在闭合大括号处自动调用这个 `drop` 函数。
> **注意**:在 C++ 中,这种在某个项目生命周期结束时解分配资源的模式,有时称为 *资源获取即初始化Resource Acquisition Is InitializationRAII*。如果咱们使用过 RAII 模式,那么 Rust 中的 `drop` 函数对咱们一定不会陌生。
> **注意**:在 C++ 中,这种在项目生命周期结束时解分配(释放)资源的模式,有时称为 *资源获取即初始化Resource Acquisition Is InitializationRAII*。若咱们曾用到 RAII 模式,那么 Rust 中的 `drop` 函数对咱们一定不会陌生。
这种模式对 Rust 代码的编写方式,影响深远。现在看来可能很简单,但在当我们打算让多个变量使用我们在堆上分配的数据,这种更为复杂的情况下,代码的行为会出乎意料。现在我们来探讨一下其中的一些情况。
这种模式对 Rust 代码的编写方式有着深刻影响。其现在看来可能很简单,但在一些更为复杂的情形下,当我们打算让多个变量使用我们在堆上分配的数据,代码的行为可能会出乎意料。现在我们来探讨一下其中的一些情况。
### 变量与数据相互作用:迁移
**Variables and Data Interacting with Move**
在 Rust 中,多个变量可以不同的方式,与同一数据交互。我们来看看清单 4-2 中,使用整数的示例。
在 Rust 中,多个变量可以不同的方式与同一数据交互。下面清单 4-2 中展示了个使用整数的示例。
<a name="listing_4-2"></a>
```rust
let x = 5;
let y = x;
```
*清单 4-2:将变量 `x` 的整数值,赋值给 `y`*
**清单 4-2**:指派变量 `x` 的整数值给 `y`
我们大致可以猜到这段代码在做什么: “绑定值 `5``x`;然后构造 `x` 中值的拷贝并绑定其到 `y`”。我们现在有两个变量 `x``y`,且都等于 `5`。这确实是正在发生的事情,因为整数属于有着已知、固定大小的简单值,且这两个值 `5` 都会被压入栈上。
我们大致可以猜到这是在做什么: “将值 `5` 绑定到 `x`;然后构造 `x` 中值的一份拷贝,并将其绑定到 `y`”。现在我们有两个变量,`x``y`,他们都等于 `5`。这确实是正在发生的事情,因为整数属于有着已知、固定大小的简单值,而这两个 `5` 值,都会被压入栈上
> **译注**:这就是下面会讲到的 [唯栈数据:拷贝](#唯栈数据拷贝) 情形
> **注**:这就是下面会讲到的 [唯栈数据:拷贝](#唯栈数据拷贝stack-only-data-copy) 情形。
现在我们来看看 `String` 这个版本:
现在我们来看看 `String` 版本:
```rust
@@ -167,51 +164,58 @@ let s1 = String::from("hello");
let s2 = s1;
```
这看起来非常相似,因此我们可能会认为其工作方式一样:即第二行构造出一份 `s1` 中值的拷贝,并将其绑定到 `s2`。但事实并非如此。
这看起来非常相似,因此我们可能会认为其工作方式一样:即第二行构造出一份 `s1` 中值的拷贝,并将其绑定到 `s2`。但事实并非如此。
> **注**:下面的代码将打印出 `s1: 你好!, s2: 你好!`,表示类型 `&str` (字符串切片)是存储在栈上的。
```rust
fn main() {
let s1 = "你好!";
let s2 = s1;
println! ("s1: {s1}, s2: {s2}");
}
```
> **注**:下面的代码将打印出 `s1: 你好!, s2: 你好!`,表示类型 `&str` (字符串切片)是存储在栈上的。
>
> ```rust
> fn main() {
> let s1 = "你好!";
> let s2 = s1;
>
> println! ("s1: {s1}, s2: {s2}");
> }
> ```
图 4-1了解 `String` 到底发生了什么。一个 `String` 由三部分组成如左所示):指向存放该字符串内容的内存的一个指针、一个长度与一个容量。这组数据,是存储在栈上的。而图的右边,则是堆上存放着内容的内存。
参阅图 4-1 了解 `String` 在幕后到底发生了什么。`String` 由三部分组成如左所示
- 指向存放字符串内容内存的指针
- 长度
- 及容量
这组数据存储在栈上。而右侧是堆上保存着内容的内存。
<a name="f_4-1"></a>
![Rust 中 `String` 类型的本质](../images/Ch04_01.svg)
*图 4-1保存绑定到 `s1` 值 `"hello"` 的 `String` 在内存中的表示*
**图 4-1**:保存绑定到 `s1``"hello"``String` 值,在内存中的表示
> **注**`String` 类似属于 [灵巧指针](../Ch15_Smart_Pointers.md),他是个包含了指针与其他一些元数据,有着一些方法特别能力的结构体。
> **注**`String` 类型类似属于 [灵巧指针](../Ch15_Smart_Pointers.md),他是保存着指针与其他一些元数据,有着一些方法特别能力的结构体。
长度是这个 `String` 内容当前使用的内存容量(以字节为单位)。容量则是这个字符串,从内存分配器获得的内存总量以字节为单位。长度容量之间的差异很重要,但在现在这个上下文中并不重要,所以忽略这个容量就可以了
所谓长度,是 `String` 内容当前使用的内存量,以字节为单位。而容量是 `String`从内存分配器收到的内存总量以字节为单位。长度容量之间的差异很重要,但在这一语境下并不重要,所以暂时忽略容量即可
当我们 `s1` 赋值`s2` 时,这个 `String` 数据会被复制,这意味着我们拷贝了栈上的指针、长度容量。我们不会拷贝指针指向的堆上数据。换句话说,内存中的数据表示,会如下图 4-2 所示。
当我们指派 `s1``s2` 时,`String` 数据会被复制,这意味着我们拷贝了全部在栈上的指针、长度容量。我们不会拷贝指针指向的堆上数据。换句话说,内存中的数据表示如下图 4-2 所示。
![有着变量 `s1` 的指针、长度与容量拷贝的变量 `s2` 在内存中的表示](../images/Ch04_02.svg)
<a name="f_4-2"></a>
![变量 `s2` 在内存中的表示,他有着 `s1` 的指针、长度与容量](../images/Ch04_02.svg)
*图 4-2变量 `s2` 在内存中的表示,有着 `s1` 的指针、长度容量的副本*
**图 4-2**:变量 `s2` 在内存中的表示,有着 `s1` 的指针、长度容量
如果 Rust 也拷贝了堆数据,那么内存的表示就不会如图 4-3 所示。如果 Rust 这样做了,那么在堆上的数据很大时,操作 `s2 = s1` 在运行时的性能方面,可能会非常昂贵。
这种表示 ** 像是下图 4-3其是当 Rust 也拷贝了堆数据时内存看起来的样子。若 Rust 真这样做了,那么当堆上的数据很大时,`s2 = s1` 这一操作在运行时性能方面就会非常昂贵。
![`s2 = s1` 操作的另一种可能Rust 拷贝内存堆数据](../images/Ch04_03.svg)
*图 4-3如果 Rust 也复制了堆数据, `s2 = s1` 可能会执行的另一种可能性*
*图 4-3 Rust 也复制了堆数据,那么 `s2 = s1` 可能会执行的另一种可能性*
早先我们曾说过当变量超出作用域时Rust 会自动调用 `drop` 函数并清理该变量的堆内存。但图 4-2 显示两个数据指针都指向同一位置。这是一个问题:当 `s2``s1` 超出作用域时,他们都将尝试释放同一内存。这成为 *双重释放double free* 错误,是我们之前提到的内存安全 bug 之一。释放两次内存会导致内存损坏,这会潜在导致安全漏洞。
前面我们说过当某个变量超出作用域时Rust 会自动调用 `drop` 函数,并清理该变量的堆内存。但图 4-2 显示,两个数据指针都指向同一位置。这就有问题了:当 `s2` `s1` 超出作用域时,他们都会尝试释放相同的内存。这就是所谓的 *双重释放double free* 错误,也是我们之前提到的内存安全漏洞之一。释放两次内存,会导致内存损坏,从而可能导致安全漏洞。
为了确保内存安全,在 `let s2 = s1;` 之后Rust 便将 `s1` 视为不再有效。因此,当 `s1` 离开作用域时Rust 不需要释放任何东西。请在` s2` 创建出之后,看看尝试使用 `s1` 时会发生什么;他不会工作:
为了确保内存安全,在 `let s2 = s1;` 行后Rust 便将 `s1` 当作不再有效。而因此,在 `s1` 超出作用域时,Rust 不需要释放任何东西。请查看咱们在 ` s2` 创建后尝试使用 `s1` 时会发生什么;他不会工作:
```rust
@@ -222,20 +226,12 @@ fn main() {
```
因为 Rust 阻止咱们使用无效引用,咱们就会得到一个下面这样的报错
咱们将得到如下错误,因为 Rust 阻止咱们使用已失效的引用
```console
$ cargo run
Compiling ownership_demo v0.1.0 (/home/peng/rust-lang-zh_CN/projects/ownership_demo)
warning: unused variable: `s2`
--> src/main.rs:3:9
|
3 | let s2 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s2`
|
= note: `#[warn(unused_variables)]` on by default
Compiling ownership v0.1.0 (/home/hector/rust-lang-zh_CN/projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:29
|
@@ -253,124 +249,149 @@ help: consider cloning the value if the performance cost is acceptable
3 | let s2 = s1.clone();
| ++++++++
warning: unused variable: `s2`
--> src/main.rs:3:9
|
3 | let s2 = s1;
| ^^ help: if this is intentional, prefix it with an underscore: `_s2`
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
For more information about this error, try `rustc --explain E0382`.
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
warning: `ownership` (bin "ownership") generated 1 warning
error: could not compile `ownership` (bin "ownership") due to 1 previous error; 1 warning emitted
```
如果咱们在使用其他语言时听说过 *浅拷贝shallow copy* *深拷贝deep copy* 这两个术语,那么拷贝指针、长度和容量,而不拷贝数据的这概念,可能听起来就像是在构造一份浅拷贝。但是,由于 Rust 还会使第一个变量失效,所以这不叫浅拷贝,而 *迁移move*。在这个例中,我们会说 `s1` 被迁移到 `s2` 中。因此,实际发生的情况如下图 4-4 所示。
咱们在使用其他语言时听说过 *浅拷贝shallow copy* *深拷贝deep copy* 这两个术语,那么拷贝指针、长度和容量,而不拷贝数据的这概念,听起来可能像在构造一份浅拷贝。但由于 Rust 还会使第一个变量失效,所以这不叫浅拷贝,而成为一次 *迁移move*。在这个例中,我们会说 `s1` 被迁移到 `s2` 中。因此,实际发生的情况如下图 4-4 所示。
![在变量 `s1` 失效后内存中的表示](../images/Ch04_04.svg)
<a name="f_4-4"></a>
![变量 `s1` 无效后内存中的表示](../images/Ch04_04.svg)
*图 4-4变量 `s1` 被失效后内存中的表示*
*图 4-4变量 `s1` 无效化后内存中的表示*
解决了我们的问题!由于只有 `s2` 有效,当他超出作用域时,只有他将释放内存,我们就大功告成了。
这解决了我们的问题!只有 `s2` 有效,当他超出作用域时,只有他将释放内存,我们就大功告成了。
此外,这还暗含一种设计取舍: Rust 绝不自动创建咱们数据的 “深” 拷贝。因此,可以假定任何 *自动* 拷贝在运行时性能方面,都是低成本的
此外,这还暗含一种设计取舍: Rust 绝不自动创建咱们数据的 “深” 拷贝。因此,任何 *自动* 拷贝在运行时性能方面,都可认为成本很低
### 作用域与赋值
作用域、所有权,与通过 `drop` 函数释放内存之间关系的反面同样成立。当咱们将某个全新值指派给一个既有变量时Rust 将立即调用 `drop` 并释放原始值的内存。例如考虑以下代码:
```rust
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
```
我们一开始声明一个变量 `s` 并将其绑定到值为 `"hello"` 的一个 `String`。随后我们立即以值 `"ahoy"` 创建一个新的 `String`,并将其指派给 `s`。此时,根本没有任何东西指向堆内存中的原始值。下图 4-5 演示了现在的栈与堆数据:
<a name="f_4-5"></a>
![初始值被完全替换后内存中的表示](../images/trpl04-05.svg)
**图 4-5**:初始值被完全替换后内存中的表示
原先的字符串因此立即超出作用域。Rust 将对其运行 `drop` 函数,其内存将立即释放。当我们最后打印该值时,其将是 `"ahoy, world!"`
### 变量与数据相互作用:克隆
**Variables and Data Interacting with Clone**
当我们 *确实* 打算深度拷贝 `String` 的堆数据,而不仅是栈数据时,我们可使用名为 `clone` 的常用方法。我们将在第 5 章讨论方法语法,但由于方法属于许多编程语言中的通用特性,因此咱们大概以前就已经见过。
在我们 *确实* 想要深度拷贝该 `String` 的堆数据,而不仅仅是其栈数据时,我们可以使用一个名为 `clone` 的常用方法。我们将在第 5 章讨论方法语法,但由于方法是许多编程语言的共同特征,你可能已经见过了。
下面是这个 `clone` 方法的一个实际应用示例:
下面是 `clone` 方法的一个实际应用示例:
```rust
let s1 = String::from("hello");
let s2 = s1.clone();
println! ("s1 = {s1}, s2 = {s2}");
println! ("
s1 = {s1}
s2 = {s2}
");
```
工作得很好,并会显式地产生图 4-3 所示的行为,其中堆数据 *确实* 得以拷贝。
段代码工作良好,并会显式地产生图 4-3 所示的行为,其中堆数据 *确实* 得以拷贝。
当咱们看到某个对 `clone` 的调用时,咱们就知道一些任意代码正在被执行,而这些代码可能很昂贵。这是个直观的指示器,表明正在发生一些不同寻常的事情。
当咱们看到对 `clone` 的调用时,咱们就知道一些任意代码正被执行,而这些代码可能开销高昂。这是一些不同寻常事情正在发生的直观指示器。
### 唯栈数据:拷贝
**Stack-Only Data: Copy**
还有一个咱们尚未谈到过的问题。使用整数的这段代码 -- 其中一部分已在清单 4-2 中给出 -- 是有效的:
还有一个咱们尚未讨论的细节。下面这段使用整数的代码 -- 部分已在清单 4-2 中给出 -- 会工作并时有效的:
```rust
let x = 5;
let y = x;
let x = 5;
let y = x;
println! ("x = {}, y = {}", x, y);
println! ("
x = {x}
y = {y}
");
```
但这段代码似乎与我们刚刚学到的内容相矛盾:我们没有调用 `clone`,但 `x `仍然有效,而没有被迁移到 `y` 中。
但这段代码似乎与我们刚了解内容相矛盾:我们没有调用 `clone`,但 `x `仍然有效且未被迁移到 `y` 中。
原因是整数这在编译时已知大小的类型,会被整个地存储在栈上,所以具体值的拷贝很快就构造出来了。换句话说,这里的深拷贝和浅拷贝没有区别,所以调用 `clone` 与通常的浅拷贝,就不会有什么不同,我们可以不使用他。
原因是诸如整数这在编译时有着已知大小的类型完全存储在栈上,所以要构造这些具体值的拷贝很快。这意味着我们没有理由在创建变量 `y` 后阻止 `x` 保持有效。换句话说,这里的深拷贝和浅拷贝没有区别,所以调用 `clone` 不会执行任何与通常的浅拷贝不同的事情,我们可以不使用他。
Rust 有个`Copy` 特质的特殊注解,我们可以把他放在像整数这种,存储在栈上的类型上(我们将在 [第 10 章](../generic_types_traits_and_lifetimes/traits.md) 详细讨论特质)。某个类型实现 `Copy` 特质时,用他的变量不会迁移,而是被简单拷贝,从而令到他们在赋值给另一变量后仍然有效。
Rust 有个`Copy` 特质的特殊注解,我们可以将其放置存储于栈中的类型上,就像整数一样(我们将在 [第 10 章](../generic_types_traits_and_lifetimes/traits.md) 详细讨论特质)。某个类型实现 `Copy` 特质时,那么使用他的变量不会迁移,而是被简单拷贝,从而使他们指派给另一变量后仍然有效。
如果某个类型或其任何部分已经实现了 `Drop` 这个特质Rust 不会让我们以 `Copy` 注解该类型了。如果该类型需要在值超出作用域时发生些特殊事情,且我们又为该类型添加 `Copy` 注解,我们将得到一个编译时报错。要了解如何 `Copy` 注解添加到咱们的类型以实现这个特质,请参阅附录 C 中的 [派生特质](../appendix/derivable_traits.md)。
类型或其任何部分已经实现了 `Drop` 特质Rust 不会让我们以 `Copy` 注解该类型。当类型需要在值超出作用域时发生些特殊事情(译注:`drop` 函数中执行代码),且我们添加 `Copy` 注解到该类型时,我们将得到一个编译时报错。要了解如何添加 `Copy` 注解添加到咱们的类型以实现特质,请参阅附录 C 中的 [派生特质](../appendix/derivable_traits.md)。
那么,哪些类型实现了 `Copy` 特质呢?咱们可查看给定类型的文档来确定,但一般来说,任何一组简单标量值都可以实现 `Copy`,而任何要求内存分配,或属于某种形式资源的类型,都不能实现 `Copy`。下面是一些实现 `Copy` 的类型:
- 所有整数类型,如 `u32`
- 布尔类型,`bool`,值为 `true``false`
- 所有浮点类型,如 `f64`
那么,哪些类型实现了 `Copy` 特质呢?咱们可查看给定类型的文档来确定,但作为一般规则,任何一组简单标量值都可以实现 `Copy`,而任何要求内存分配某种形式资源的类型都无法实现 `Copy`。下面是一些实现 `Copy` 的类型:
- 所有整数类型,比如 `u32`
- 布尔类型,`bool`,取值为 `true``false`
- 所有浮点类型,比如 `f64`
- 字符类型 `char`
- 只包含同时实现了 `Copy` 类型的元组。例如,`(i32, i32)` 实现了 `Copy`,但 `(i32, String)` 则没有。
- 元组,当其只包含同时实现 `Copy` 的类型时。比如,`(i32, i32)` 实现了 `Copy`,但 `(i32, String)` 则没有。
## 所有权与函数
**Ownership and Functions**
将某个值传递给函数的机制,与为将一个值赋给某个变量类似。将一个变量传递给某个函数,会像赋值一样,进行迁移或拷贝。下面清单 4-3 有着一个,带有一些显示其中变量进入和超出作用域位置注释的示例。
传递值到函数的机制与赋值给变量时的机制类似。传递变量给函数将迁移或拷贝,就像复制所做的那样。下面清单 4-3 有个示例,带有一些显示变量于何处进入与超出作用域的注释。
<a name="listing_4-3"></a>
文件名:`src/main.rs`
```rust
fn main() {
let s = String::from("hello"); // s 进作用域
let s = String::from("hello"); // s 进作用域
takes_ownership(s); // 变量 s 的值迁移到这个函数中......
// ......进而在这里不再有效
let x = 5; // x 进作用域
let x = 5; // x 进作用域
makes_copy(x); // x 将迁移到这个函数中
// 但由于 i32 实现了 `Copy` 特质,因此
// 后面使用变量 x 没有问题
makes_copy(x); // 由于 i32 实现了 `Copy` 特质
// x 不会迁移到这个函数中,
// 因此后面使用 x 没有问题
println! ("x = {x}");
} // 这里x 超出作用域,接着是 s。但由于 s 的值已被迁移,因此
// 不会有特别事情发生。
} // 这里x 超出作用域,接着是 s。但由于 s 的值已被迁移,因此
// 不会有特别事情发生。
fn takes_ownership(some_string: String) { // some_string 进作用域
fn takes_ownership(some_string: String) { // some_string 进作用域
println! ("{}", some_string);
} // 这里some_string 超出作用域,而 `drop` 方法会被调用。退回
} // 这里some_string 超出作用域 `drop` 被调用。归还
// 内存被释放。
fn makes_copy(some_integer: i32) { // some_integer 进作用域
fn makes_copy(some_integer: i32) { // some_integer 进作用域
println! ("{}", some_integer);
} // 这里some_integer 超出作用域。没有特别事情发生。
```
*清单 4-3:带有所有权与作用域注解的函数*
**清单 4-3**:注解了所有权与作用域的一些函数
若我们试图在调用 `takes_ownership` 后使用 `s`Rust 将抛出一个编译时错误。这些静态检查保护我们免于犯错。请尝试添加使用 `s``x` 的代码到 `main` 中,看看咱们可于何处使用他们,以及所有权规则会在哪里会阻止咱们这样做。
> **注**:下面的代码,仍然会报出:`use of moved value: ``some_string```错误:
> **注**:下面的代码,仍然会报出:`use of moved value: ``some_string``` 错误:
```rust
@@ -381,17 +402,12 @@ fn takes_ownership(some_string: String) {
}
```
如果我们试图在调用 `takes_ownership` 后使用 `s`Rust 就会抛出一个编译时报错。这些静态检查,可以防止我们犯错。请尝试在 `main` 中,添加使用 `s``x` 的代码,看看在哪些地方咱们可以使用他们,在哪些地方所有权规则会阻止咱们这样做。
## 返回值与作用域
**Return Values and Scope**
返回值也会转移所有权。下面清单 4-4 给出了一个返回某个值的函数示例,有着与清单 4-3 中类似的注释。
返回值也会转移所有权。下面清单 4-4 显示了个返回某个值的函数示例,带有与清单 4-3 中类似的注释。
<a name="listing_4-4"></a>
文件名:`src/main.rs`
```rust
@@ -402,14 +418,14 @@ fn main() {
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_bake(s2); // s2 会被迁移到 takes_and_gives_back
// 中,其又会将他的返回值迁移到 s3 中
// 中,他又其返回值迁移到 s3 中
println! ("s1: {s1}, s3: {s3}");
} // 这里s3 超出作用域而被丢弃。变量 s2 已被迁移,因此什么也不会发生。
// s1 超出作用域而被丢弃。
} // 这里s3 超出作用域而被丢弃。变量 s2 已被迁移,因此什么也不会发生。
// s1 超出作用域而被丢弃。
fn gives_ownership() -> String { // gives_ownership 将把他的返回值,迁移
// 到调用他的函数中(即 main 函数)
fn gives_ownership() -> String { // gives_ownership 将把迁移其返回值
// 到调用他的函数中
let some_string = String::from("归你了"); // some_string 进到作用域
@@ -417,22 +433,23 @@ fn gives_ownership() -> String { // gives_ownership 将把他的
// 迁出到调用函数
}
// 函数取个 String 并会返回一个 String
// 这个函数取个 String 并会返回一个 String
fn takes_and_gives_bake(a_string: String) -> String { // a_string 进入作用域
a_string // a_string 被返回,并迁出到调用函数
a_string // a_string 被返回,并迁出到调用函数
}
```
*清单 4-4转移返回值的所有权*
**清单 4-4**:转移返回值的所有权
变量的所有权每次都会遵循同一模式:将某个值赋值另一变量,就会迁移他。当某个包含了堆中数据的变量超出作用域时,除非数据的所有权已迁移到另一变量,否则该值就将被 `drop` 清理
变量的所有权每次都会遵循同一模式:赋值另一变量会迁移他。当包含堆上数据的变量超出作用域时,该值将被 `drop` 清理,除非数据的所有权已迁移到另一变量。
虽然这种模式可行,但在每个函数中取得所有权,并返回所有权会有繁琐。如果我们打算让某个函数使用一个值,但又不打算取得所有权,该怎么办呢?如果我们传入的任何数据,在我们打算再次使用时,都需要传回,此外,我们可能还打算返回函数主体产生的任何数据,这一点非常恼人
虽然这种模式可行,但在每个函数中取得所有权然后返回所有权会有繁琐。若我们想要让函数使用值却不取得所有权,该怎么办呢?非常烦人的时,当我们希望再次使用我们传入的任何数据,以及我们可能希望返回函数主体产生的任何数据,都需要被传回
Rust 确实允许咱们使用元组返回多个值,如下清单 4-5 中所示。
<a name="listing_4-5"></a>
文件名:`src/main.rs`
```rust
@@ -451,9 +468,9 @@ fn calculate_length(s: String) -> (String, usize) {
}
```
*清单 4-5返回参数的所有权*
**清单 4-5**:返回参数的所有权
是,对于一个本应常见的概念来说,这样做太过仪式化和工作量都太大了。幸运的是Rust 有一种使用某个值,而不转移所有权的特性,叫做 *引用references*
但对于一个本应常见的概念来说,这太过繁琐且属于大量工作。幸运的是Rust 有一项可在无需转移所有权下使用值的特性:*引用references*
End

View File

@@ -168,7 +168,7 @@ fn main() {
}
```
*清单 3-2将 `if` 表达式的结果赋值给变量*
**清单 3-2**:将 `if` 表达式的结果赋值给变量
其中的 `number` 变量将根据 `if` 表达式的结果绑定到某个值。请运行这段代码,看看会发生什么:
@@ -409,7 +409,7 @@ fn main() {
}
```
*清单 3-3使用 `while` 循环在条件求值为 `true` 时运行代码*
**清单 3-3**:使用 `while` 循环在条件求值为 `true` 时运行代码
这种结构消除了咱们在使用 `loop``if``else`、及 `break` 时所需的大量嵌套,且更为清晰。在条件求值为 `true` 时,代码运行;否则,他会退出循环。
@@ -420,6 +420,7 @@ fn main() {
咱们可选择使用 `while` 结构遍历集合中的元素,比如数组。例如,下面清单 3-4 中的循环会打印数组 `a` 中的各个元素。
<a name="listing_3-4"></a>
文件名:`src/main.rs`
```rust
@@ -435,7 +436,7 @@ fn main() {
}
```
*清单 3-4使用 `while` 循环遍历集合中的各个元素*
**清单 3-4**:使用 `while` 循环遍历集合中的各个元素
此处,代码会对数组中的元素向上计数。他会于索引 `0` 处开始然后循环,直到他到达数组中的最终索引(即,当 `index < 5` 不再为 `true` 时)。运行这段代码将打印该数组中的每个元素:
@@ -474,7 +475,7 @@ fn main() {
}
```
*清单 3-5使用 `for` 循环遍历集合中的各个元素*
**清单 3-5**:使用 `for` 循环遍历集合中的各个元素
当咱们运行这段代码时,我们将看到与清单 3-4 同样的输出。更重要的是,我们现在提高了代码的安全性,并消除了超出数组末尾,或因遍历不足远而遗漏某些项目等可能导致的 bug 的可能性。自 `for` 循环生成的机器码也会更加高效,因为索引不需要在每次迭代时都与数组长度比较。

View File

@@ -51,8 +51,8 @@ error: could not compile `type_annotation` (bin "type_annotation") due to 1 prev
所谓 *整数integer*,属于不带小数部分的数字。我们曾在第 2 章中用到一种整数类型,即 `u32` 类型。这种类型声明表明,与之关联的值应是个占用 32 位空间的无符号整数(有符号整数类型以 `i` 而不是 `u` 开头)。下表 3-1 列出了 Rust 中内置的整数类型。我们可使用任何的这些变种,声明某个整数值的类型。
<a name="table-3-1"></a>
*表 3-1Rust 中的整数类型*
<a name="table_3-1"></a>
**表 3-1**Rust 中的整数类型
| 长度 | 有符号 | 无符号 |
@@ -74,7 +74,8 @@ error: could not compile `type_annotation` (bin "type_annotation") due to 1 prev
咱们可以下表 3-2 所示的任何形式,书写整数的字面值。要注意可能是多种数值类型的数字字面值,则允许使用类型后缀来指定类型,比如 `57u8`。数字字面值还可使用 `_` 作为视觉分隔符,使数字更容易读取,例如 `1_000`,其将有着与咱们指定 `1000` 时相同的值。
*表 3-2Rust 中的数字字面值*
<a name="table_3-2"></a>
**表 3-2**Rust 中的数字字面值
| 数字字面值 | 示例 |
| :- | :- |

View File

@@ -124,7 +124,7 @@ fn main() {
}
```
*清单 3-1包含一条语句的 `main` 函数声明*
**清单 3-1**:包含一条语句的 `main` 函数声明
函数定义也属于语句;整个上面的示例本身就是条语句。