diff --git a/projects/ownership/src/main.rs b/projects/ownership/src/main.rs index 6f89f5b..9d861a2 100644 --- a/projects/ownership/src/main.rs +++ b/projects/ownership/src/main.rs @@ -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) } diff --git a/src/Ch02_Programming_a_Guessing_Game.md b/src/Ch02_Programming_a_Guessing_Game.md index 0755072..d323530 100644 --- a/src/Ch02_Programming_a_Guessing_Game.md +++ b/src/Ch02_Programming_a_Guessing_Game.md @@ -1,7 +1,5 @@ # 编写猜数游戏 -**Programming a Guessing Game** - 咱们来一起完成一个实践项目,开始学习 Rust!这一章通过向咱们展示如何在实际程序中如何运用他们,向咱们介绍一些常见 Rust 概念。咱们将了解 @@ -80,6 +78,7 @@ Hello, world! 这个猜数游戏程序的第一部分,将请求用户输入,处理该输入并检查输入是否为预期形式。首先,我们将允许玩家输入一个猜测。请将清单 2-1 中的代码,输入到 `src/main.rs` 中。 + 文件名:`src/main.rs` ```rust @@ -100,8 +99,7 @@ fn main() { } ``` - -*清单 2-1,从用户处获取一个猜数并将其打印出来的代码* +**清单 2-1**,从用户处获取一个猜数并将其打印出来的代码 这段代码包含了大量信息,所以我们来逐行查看。要获取用户输入,然后将结果作为输出打印,我们就需要将 `io` 这个输入/输出库,带入作用域。`io` 库来自标准库,称为 `std`: @@ -313,6 +311,7 @@ Cargo 认为这些版本有着与 `0.8.5` 版本兼容的公共 API,而这一 现在,在不修改任何代码下,我们来构建这个项目,如清单 2-2 中所示。 + ```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 ``` - -*清单 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 中所示。 + 文件名:`src/main.rs` ```rust @@ -455,8 +454,7 @@ fn main() { } ``` - -*清单 2-3:添加生成随机数的代码* +**清单 2-3**:添加生成随机数的代码 首先,我们要添加行 `use rand::Rng;`。`Rng` 这个特质定义了随机数生成器所实现的一些方法,而这个特质必须要在咱们要用到那些方法的作用域中。[第 10 章](./generic_types_traits_and_lifetimes/traits.md) 将详细介绍特质。 @@ -502,6 +500,7 @@ $ cargo run 现在我们有了用户输入和随机数,我们可以比较他们。该步骤于清单 2-4 中所示。请注意,这段代码还不会编译,我们将进行解释。 + 文件名:`src/main.rs` ```rust @@ -521,8 +520,8 @@ fn main() { } } ``` - -*清单 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 中所示。 + 文件名:`src/main.rs` ```rust @@ -756,8 +756,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace // --跳过-- ``` - -*清单 2-5:忽略非数字的猜数并请求另一猜数,而不是让崩溃程序* +**清单 2-5**:忽略非数字的猜数并请求另一猜数,而不是让崩溃程序 我们从 `expect` 调用切换到 `match` 表达式,以从出错时崩溃程序转而处理错误。请记住,`parse` 会返回一个 `Result` 类型,而 `Result` 是个枚举,有着 `Ok` 和 `Err` 两个变种。我们在这里使用了个 `match` 表达式,就像我对 `cmp` 方法的 `Ordering` 结果所做的一样。 @@ -788,6 +787,7 @@ adb 太棒了!只需最后一个微小调整,我们即将完成这个猜数游戏了。回想一下这个程序仍在打印秘密数字。这对测试来说效果很好,但却毁掉了这个游戏。咱们来删除那个输出秘密数字的 `println!`。清单 2-6 给出了最终代码。 + 文件名:`src/main.rs` ```rust @@ -829,8 +829,7 @@ fn main() { } ``` - -*清单 2-6:完整的猜数游戏代码* +**清单 2-6**:完整的猜数游戏代码 至此,咱们已成功构建了这个猜数游戏。恭喜! diff --git a/src/getting_started/hello_cargo.md b/src/getting_started/hello_cargo.md index fb35fcc..8217363 100644 --- a/src/getting_started/hello_cargo.md +++ b/src/getting_started/hello_cargo.md @@ -35,6 +35,7 @@ $ cd hello_cargo 在咱们所选的文本编辑器中,打开 `Cargo.toml`。他应该类似于清单 1-2 中的代码。 + 文件名:`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 会帮咱们管理其内容。 + 文件名:`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`,在一条命令中,编译代码并随后运行得到的可执行文件: diff --git a/src/getting_started/hello_world.md b/src/getting_started/hello_world.md index 82280f3..530e084 100644 --- a/src/getting_started/hello_world.md +++ b/src/getting_started/hello_world.md @@ -39,6 +39,7 @@ $ cd hello_world 现在打开咱们刚创建的 `main.rs` 文件,输入下面清单 1-1 中的代码。 + 文件名:`main.rs` ```rust @@ -47,7 +48,7 @@ fn main() { } ``` -*清单 1-1:打印 `Hello, World!` 的一个程序* +**清单 1-1**:打印 `Hello, World!` 的一个程序 请保存该文件,并返回到 `~/projects/hello_world` 目录下,咱们的终端窗口。在 Linux 或 macOS 上输入以下命令,编译并运行该文件: diff --git a/src/images/trpl04-05.svg b/src/images/trpl04-05.svg new file mode 100644 index 0000000..f3c6e8a --- /dev/null +++ b/src/images/trpl04-05.svg @@ -0,0 +1,95 @@ + + + + + + + + +cluster_heap + + + +s + +s + +name + +value + +ptr + + +len + +4 + +capacity + +4 + + + +ahoy + +index + +value + +0 + +a + +1 + +h + +2 + +o + +3 + +y + + + +s:c->ahoy:pointee + + + + + +hello + + +index + +value + +0 + +h + +1 + +e + +2 + +l + +3 + +l + +4 + +o + + + diff --git a/src/ownership/about_ownership.md b/src/ownership/about_ownership.md index 3ac95b6..1a4367f 100644 --- a/src/ownership/about_ownership.md +++ b/src/ownership/about_ownership.md @@ -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 collector,GC* 的语言中,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 Initialization,RAII*。如果咱们使用过 RAII 模式,那么 Rust 中的 `drop` 函数对咱们一定不会陌生。 +> **注意**:在 C++ 中,这种在项目生命周期结束时解分配(释放)资源的模式,有时称为 *资源获取即初始化,Resource Acquisition Is Initialization,RAII*。若咱们曾用到 RAII 模式,那么 Rust 中的 `drop` 函数对咱们一定不会陌生。 -这种模式对 Rust 代码的编写方式,影响深远。现在看来可能很简单,但在当我们打算让多个变量,使用我们在堆上分配的数据,这种更为复杂的情况下,代码的行为就会出乎意料。现在我们来探讨一下,其中的一些情况。 +这种模式对 Rust 代码的编写方式有着深刻影响。其现在看来可能很简单,但在一些更为复杂的情形下,当我们打算让多个变量使用我们在堆上分配的数据时,代码的行为可能会出乎意料。现在我们来探讨一下其中的一些情况。 ### 变量与数据相互作用:迁移 -**Variables and Data Interacting with Move** - - -在 Rust 中,多个变量可以不同的方式,与同一数据交互。我们来看看清单 4-2 中,使用整数的示例。 +在 Rust 中,多个变量可以不同的方式与同一数据交互。下面清单 4-2 中展示了个使用整数的示例。 + ```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` 值由三部分组成,如左侧所示: + +- 指向存放字符串内容内存的指针 +- 长度 +- 及容量 +这组数据存储在栈上。而右侧是堆上保存着内容的内存。 + + + ![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) + +![变量 `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) + +![变量 `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 演示了现在的栈与堆数据: + + +![初始值被完全替换后内存中的表示](../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 有个示例,带有一些显示变量于何处进入与超出作用域的注释。 + 文件名:`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 中类似的注释。 + 文件名:`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 中所示。 + 文件名:`src/main.rs` ```rust @@ -451,9 +468,9 @@ fn calculate_length(s: String) -> (String, usize) { } ``` -*清单 4-5:返回参数的所有权* +**清单 4-5**:返回参数的所有权 -但是,对于一个本应很常见的概念来说,这样做太过仪式化和工作量都太大了。幸运的是,Rust 有一种使用某个值,而不转移所有权的特性,叫做 *引用,references*。 +但对于一个本应常见的概念来说,这太过繁琐且属于大量工作。幸运的是,Rust 有一项可在无需转移所有权下使用值的特性:*引用,references*。 (End) diff --git a/src/programming_concepts/control_flow.md b/src/programming_concepts/control_flow.md index 82c2e7e..9593edc 100644 --- a/src/programming_concepts/control_flow.md +++ b/src/programming_concepts/control_flow.md @@ -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` 中的各个元素。 + 文件名:`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` 循环生成的机器码也会更加高效,因为索引不需要在每次迭代时都与数组长度比较。 diff --git a/src/programming_concepts/data_types.md b/src/programming_concepts/data_types.md index 372c5cc..db3bfef 100644 --- a/src/programming_concepts/data_types.md +++ b/src/programming_concepts/data_types.md @@ -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 中内置的整数类型。我们可使用任何的这些变种,声明某个整数值的类型。 - -*表 3-1:Rust 中的整数类型* + +**表 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-2:Rust 中的数字字面值* + +**表 3-2**:Rust 中的数字字面值 | 数字字面值 | 示例 | | :- | :- | diff --git a/src/programming_concepts/functions.md b/src/programming_concepts/functions.md index d87e48c..c665a0a 100644 --- a/src/programming_concepts/functions.md +++ b/src/programming_concepts/functions.md @@ -124,7 +124,7 @@ fn main() { } ``` -*清单 3-1:包含一条语句的 `main` 函数声明* +**清单 3-1**:包含一条语句的 `main` 函数声明 函数定义也属于语句;整个上面的示例本身就是条语句。