Updated src/ownership/about_ownership.md'.

This commit is contained in:
Hector PENG
2026-03-08 18:50:34 +08:00
parent aaa6464990
commit ed128e1302
6 changed files with 59 additions and 86 deletions

View File

@@ -0,0 +1,6 @@
[package]
name = "ownership"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -0,0 +1,7 @@
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 方法会追加一个字面值,到某个 String
println! ("{}", s); // 这将打印出 `hello, world!`
}

View File

@@ -1,8 +0,0 @@
[package]
name = "ownership_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@@ -1,9 +0,0 @@
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}

View File

@@ -1,7 +1,5 @@
# 掌握所有权
# 掌握所有权
**Understanding Ownership**
所有权是 Rust 最独特的特性,对语言的其他部分有着深刻的影响。他使 Rust 可以在不需要垃圾回收器的情况下,保证内存安全,因此了解所有权的工作原理非常重要。在本章中,我们将讨论所有权,以及几个相关特性:借用、切片,与 Rust 如何将数据放置于内存中。
所有权是 Rust 最独特的特性,对这门语言的其余部分有着深远影响。他使 Rust 可以在不需要垃圾回收器下保证内存安全,因此理解所有权的工作原理非常重要。在这一章中,我们将讨论所有权以及数个相关特性:借用、切片以及 Rust 如何在内存中布局数据。

View File

@@ -1,98 +1,81 @@
# 何谓所有权
# 什么是所有权
**What is Ownership**?
所谓 *所有权ownership*,属于一套规则,控制 Rust 程序管理内存方式。所有程序在运行时都必须管理他们使用计算机内存的方式。一些语言有着垃圾回收在其运行时定期查找不再使用的内存在别的语言中程序员则必须显式分配及释放内存。Rust 使用第三种方法:在一套编译器会检查的规则下,内存经由所有权系统得以管理。当违反任何规则时,程序将不会编译。所有权的任何特性都不会在咱们程序运行时,减慢其运行速度。
因为所有权对于许多程序员来说都是个新概念,所以确实需要一些时间适应。好消息是,咱们对 Rust 与所有权系统规则越有经验,咱们就将发现自然而然地开发出安全高效代码就越容易。请坚持下去!
当咱们掌握所有权后,咱们将为掌握那些令到 Rust 独特的功能打下了坚实基础。在这一章中,咱们将通过一些示例了解所有权,这些示例重点关注极为常见的数据结构:字符串。
所谓 *所有权ownership*,是一套掌管着 Rust 程序如何管理内存的规则。所有程序在其运行期间都必须管理他们使用计算机内存的方式。有些语言有着可在程序运行时定期查找不再使用的内存的垃圾回收而在其他语言中程序员则必须明确分配和释放内存。Rust 使用了第三种方法:通过带有编译器会检查的一套规则的所有权系统,内存得以管理。如果违反了任何规则,程序将不会编译。在程序运行过程中,所有权的所有特性,都不会减慢程序的运行速度。
由于对于许多程序员来说,所有权是个新概念,因此需要一些时间来适应。好消息是,咱们对 Rust 和所有权系统规则越有经验,咱们就会发现,自然而然地开发出安全高效的代码就越容易。请坚持下去!
当咱们理解了所有权,就为理解 Rust 独特的功能,打下了坚实的基础。在本章中,咱们将通过一些以字符串,这种常见数据结构为重点的示例,来掌握所有权。
> **内存栈与堆**
> **内存栈与内存堆**
>
> **The Stack and the Heap**
> 许多编程语言都不要求咱们经常考虑栈与堆。但在像 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*。栈上存储的所有数据,必须有已知、固定的大小。编译时大小未知,或大小可能改变的数据必须存储在堆上
> 堆的组织程度较低:当咱们将数据放入堆上时,咱们会请求一定数量的空间。内存分配器会在堆中找到一个足够大的空位,将其标记为在用,并返回一个 *指针pointer*,即那个位置的地址。这个过程称为 *在堆上分配allocating on the heap*,有时简称 *分配allocating*(将值压入栈不被视为分配)。因为指向堆的指针是已知、固定大小,所以咱们可将指针存储在栈上,但是当咱们需要具体数据必须跟随指针。设想在餐厅等待安排座位的情景。当咱们进门时,咱们会说明咱们团体的人数,然后接待员会找到一张能座下每个人的空桌并把咱们领到那里。当咱们团队中有人来晚了,他们可以询问咱们被安排座位于何处以找到咱们
>
> 堆的组织程度则较低:在咱们把数据放在堆上时,咱们要请求一定数量的空间。内存分配器会在堆中,找到足够大的空位,将其标记为在用,并返回一个 *指针pointer*,即那个位置的地址。这个过程称为 *在堆上分配allocating on the heap*,有时也简称为 *分配allocating*(将值推入栈,则不被视为分配)。由于到堆的指针,属于已知、固定的大小,因此咱们可以将该指针,存储在栈上,而当咱们需要具体数据时,就必须跟随这个指针。请设想在餐厅等待安排座位的情景。当咱们进入某家餐厅时,咱们要说明咱们团体的人数,然后接待员会找到一张适合每个人的空桌,并把咱们领到那里。如果咱们团队中有人来晚了,他们可以询问,咱们的座位在哪里,然后找到咱们
> 压入栈比在堆上分配更快,因为分配器永远不必搜索存储新数据的位置;该位置始终是在栈的顶部。相比之下,在堆上分配空间需要更多的工作,因为分配器必须首先找到一个足够大的空间来容纳数据,然后进行簿记,为下一次分配做准备
>
> 压入栈要比在堆上分配空间更快,因为分配器无需寻找存储新数据的位置;该位置总是在栈的顶部。相比之下,在堆上分配空间,则需要更多的工作,因为分配器必须首先找到一个足够大的空间来存放数据,然后进行簿记,为下一次分配做好准备
> 访问堆中的数据一般慢于访问栈上的数据,因为咱们必须顺着指针才能到达那里。若现代处理器在内存中的跳转更少,那么他们就会更快。继续那个比喻,设想一名餐厅服务员在收取许多餐桌上的菜单。先处理一张桌子上的所有点餐,然后再处理下一张桌子上的点餐是最高效的。收取餐桌 A 一个菜单,然后 B 桌的一个菜单,然后再是 A 桌的一个菜单,然后再是 B 桌的一个菜单,将是慢得多的一个过程。出于同样原因,当处理器处理靠近其他数据,正如栈上那样,而不是较远的数据,正如堆上那样时,那么他通常就能更好地完成工作
>
> 访问堆中的数据,比访问栈上的数据要慢,因为咱们必须跟随指针才能到达那里。如果减少在内存中的跳转,那么现代处理器的速度就会更快。继续类比,请设想一下某个餐厅的服务员,从许多桌子上点菜的情况。最有效的方法是先处理一张桌子上的所有点餐,然后再处理下一张桌子上的点餐。从 A 桌点菜,然后从 B 桌点菜,然后再从 A 桌点菜,然后再从 B 桌点菜,这个过程就会慢得多。同样,如果处理器处理的数据,与其他数据距离较近(如栈上的数据),而不是较远(如堆中的数据),那么处理器就能更好地完成工作
> 当咱们的代码调用函数时,传入函数的值(可能包括指向堆上数据的指针)和函数的局部变量会被压入栈。在函数结束后,这些值会从栈上弹出
>
> 当咱们的代码调用某个函数时,传入函数的值(可能包括指向堆上数据的指针)和函数的局部变量,会被推入栈。函数结束后,这些值会从栈上弹出
>
> 跟踪代码的哪些部分,正在使用堆上的哪些数据、尽量减少堆上的重复数据量,以及清理堆上未使用数据以免空间耗尽,这些都是所有权要解决的问题。一旦咱们掌握了所有权,咱们就不需要经常考虑栈和堆了,而清楚所有权的主要目的,是为管理堆数据这一点,有助于解释为什么他以这种方式工作。
> 跟踪代码的哪些部分正在使用堆上的哪些数据、最大限度减少堆上的重复数据量,以及清理堆上的未使用数据以免空间耗尽等,这些都是所有权要解决的问题。一旦咱们掌握所有权,咱们就不需要经常考虑栈和堆。而知道所有权的主要目的是管理堆数据,有助于解释为什么他会这样工作
## 所有权规则
**Ownership Rules**
首先,我们来看看这些所有权规则。在我们举例说明他们时,请牢记这些规则:
- Rust 中的每个值,都有个 *所有者owner*
- 同一时间,只能有一个所有者;
首先,我们来看看所有权规则。在我们举例说明他们时,请牢记这些规则:
- Rust 中的每个值都有个 *所有者owner*
- 同一时间只能有一个所有者;
- 当所有者超出作用域时,该值将被丢弃。
## 变量作用域
**Variable Scope**
既然我们已经掌握了基本的 Rust 语法,我们将不在示例中包含所有 `fn main() {` 代码,所以若咱们在跟进学习,请务必手动将以下示例放入 `main` 函数中。如此,我们的示例将更加简洁,让我们专注于具体细节而不是样板代码了。
既然我们已经掌握了 Rust 的基本语法,我们就不会在示例中,包含所有 `fn main() {` 代码,所以如果咱们正在学习,请务必手动将下面的示例,放在某个 `main` 函数中。如此,我们的示例将更加简洁,让我们专注于具体细节,而不是样板代码了。
作为所有权的首个例子,我们来看看,一些变量的作用域。作用域是指某个项目在程序中,有效的范围。以下面的变量为例:
作为所有权的首个示例,我们将探讨一些变量的作用域。所谓 *作用域scope*,是程序中某个项目有效的范围。请看以下这个变量:
```rust
let s = "hello";
let s = "hello";
```
变量 `s` 指向个字符串字面值,其中字符串的值被硬编码到我们程序的文本中。这个变量从其被声明时开始,直到当前作用域结束,都是有效的。下面清单 4-1 给出了一个带有说明这个变量 `s` 的于何处有效注释的程序
这个变量 `s` 指向个字符串字面值,其中字符串的值被硬编码到我们程序的文本中。这个变量从其被声明处便有效,直到当前作用域结束。下面清单 4-1 展示了一个带有注释的程序,注释了变量 `s` 的于何处有效。
<a name="listing_4-1"></a>
```rust
{ // 变量 s 在这里是无效的,他还没被声明出来
let s = "hello"; // s 自此往下都是有效的
{ // 变量 s 在这里是无效的,他还没被声明出来
let s = "hello"; // s 自此往下都是有效的
// 对变量 s 执行一些操作
} // 此时该作用域结束,而变量 s 不再有效
// 对变量 s 执行一些操作
} // 此时该作用域结束,而变量 s 不再有效
```
*清单 4-1一个变量及其有效的作用域*
*清单 4-1变量及其有效的作用域*
换句话说,这里有两个重要的时间点:
-`s` ** 作用域时,他便有效了;
- 在超 ** 作用域之前,他会一直有效。
-`s` ** 作用域时,他便有效了;
- 他会保持有效知道他 *超出作用域*
此时,作用域变量何时有效之间的关系,与其他编程语言中的类似。现在我们将通过引入 `String` 类型,这种理解的基础上构建
此时,作用域变量何时有效之间的关系,与其他编程语言中的类似。现在我们将通过引入 `String` 类型,构建于这种理解的基础上。
## `String` 类型
**The `String` Type**
为了说明所有权规则,我们需要一种比第 3 章的 [数据类型](../programming_concepts/data_types.md) 小节中介绍的数据类型更复杂的数据类型。前面介绍的类型都是已知大小的,可存储在栈上,并会在其作用域结束时从栈上弹出,并当代码的另一部分需要在不同作用域下使用同一个值时,可被快速而简单地复制以创建一个新的独立实例。但我们打算看看存储在堆上的数据,并探讨 Rust 如何知道何时清理这些数据,而 `String` 类型就是个很好的例子。
我们将重点关注 `String` 中与所有权相关的部分。这些方面也适用于其他复杂数据类型,无论他们是由标准库提供还是由咱们创建出的。我们将在 [第 8 章](../common_collections/strings.md) 中讨论 `String` 的非所有权方面。
为了说明所有权规则,我们需要一种比第 3 章 [数据类型](../programming_concepts/data_types.md) 小节中,介绍的数据类型更复杂的一种数据类型。前面介绍的类型大小已知,可被存储在栈上,并在其作用域结束时,从栈上弹出,如果代码的另一部分,需要在不同的作用域中使用同一个值,他们就可以快速、简便地复制,以创建一个新的、独立的实例。但我们打算看看存储在堆上的数据,并探讨 Rust 如何知道,何时清理这些数据,而 `String` 类型就是个很好的例子。
我们将重点关注 `String` 中,与所有权相关的部分。这些方面也适用于其他复杂的数据类型,无论它们是由标准库提供,还是由咱们自己创建。我们将在 [第 8 章](../common_collections/strings.md) 中,更深入地讨论 `String`
我们已经见过字符串的字面值其中某个字符串值被硬编码到咱们的程序中。字符串字面值很方便但他们并不适合我们打算使用文本的所有情况。其中一个原因是他们是不可变的。另一个原因是在我们编写代码时并非每个字符串值都是已知的例如如果我们打算获取用户输入并将其存储起来该怎么办针对这些情况Rust 有着第二种字符串类型 -- `String`。该类型管理堆上分配的数据,因此可以存储编译时咱们未知数量的文本。咱们可以使用 `from` 函数,从某个字符串字面值创建出一个 `String`,如下所示:
我们已经见过字符串的字面值其中字符串值被硬编码到咱们程序中。字符串字面值很方便但他们并不适合我们可能打算使用文本的所有情况。原因之一是他们属于不可变的。另一个原因是当我们编写代码时并非每个字符串值都是已知的例如若我们打算获取用户输入并存储他怎么办正是针对这些情况Rust 才有了 `String` 类型。这种类型管理分配于堆上的数据,因此能够存储编译时我们未知的大量文本。咱们可以使用 `from` 函数从字符串字面值创建出 `String`,如下所示:
```rust
@@ -101,9 +84,9 @@ let s = String::from("hello");
```
其中双冒号 `::` 操作符允许我们`String` 类型下,命名这个特殊的 `from` 函数,而不是使用诸如 `string_from` 的某种名字。我们将在第 5 章的 [方法语法](../structs/method_syntax.md) 小节,及第 7 章的 [用于引用模块树中某个项目的路径](../packages_crates_and_modules/paths.md) 小节讨论模块命名空间时,进一步讨论这种语法。
其中双冒号 `::` 操作符允许我们将这个特定的 `from` 函数,命名空间到 `String` 类型下,而不是使用像是 `string_from` 的某种名字。我们将在第 5 章的 [方法语法](../structs/method_syntax.md) 小节,及第 7 章的 [用于引用模块树中某个项目的路径](../packages_crates_and_modules/paths.md) 小节讨论模块命名空间时,进一步讨论这种语法。
这种字符串*可* 被改变:
这种字符串 ** 被改变:
```rust
let mut s = String::from("hello");
@@ -114,29 +97,25 @@ let s = String::from("hello");
```
那么,这里有什么区别呢?为什么 `String` 可被改变而字面值却不能呢?区别在于这两种类型如何处理内存。
那么,这里有区别呢?为什么 `String` 可被改变而字面值却不能呢?区别在于这两种类型处理内存的方式不同
## 内存与内存分配
**Memeory and Allocation**
在字符串字面值的情况下,我们在编译时知道内容,因此文本被直接硬编码到最终的可执行文件中。这就是字符串字面值快速且高效的原因。但这些属性仅来自于字符串字面值的不变性。不幸的是,我们无法为在编译时大小未知,且在运行程序时大小可能改变的每段文本,都添加一块内存到二进制程序中。
`String` 类型下,为了支持某段可变、可增长的文本片段,我们需要在堆上分配某一数量(在编译时未知)的内存来保存内容。这意味着:
对于字符串字面值,我们在编译时就知道其内容,因此该文本会被直接硬编码到,最终的可执行文件中。这就是字符串字面值快速高效的原因。但这些属性,只是来自于字符串字面值的不变性。遗憾的是,我们无法为每段编译时大小未知,且在运行程序时大小可能改变的文本,添加一块内存到二进制程序中。
而在 `String` 类型下,为了支持某段可变、可增长的文本,我们需要在堆上,分配编译时未知的某个数量的内存,来保存内容。这意味着:
- 这一内存必须在运行时请求自内存分配器;
- 在我们使用我们的 `String` 完毕后,我们需要一种方法将这一内存归还给分配器。
- 必须在运行时,向内存分配器申请该内存;
第一部分由我们完成:当我们调用 `String::from` 时,其实现会请求其所需的内存。这在编程语言中非常普遍。
- 我们需要某种,当咱们是使用完咱们的 `String` 后,将此内存返回给分配器的方法
然而,第二部分则有所不同。在带有 *垃圾回收器garbage collectorGC* 的语言中GC 会跟踪并清理不再使用的内存,我们需要考虑他。在大多数没有 GC 的语言中,我们有责任识别内存何时不再被使用,并调用代码显式释放他,就像我们请求他一样。正确做到这一点,历来是编程难题。当我们忘记了了,我们将浪费内存。当我们过早进行时,我们将有着无效变量。当我们做了两次时,那也是个 bug。我们需要准确地将一次 `allocate` 与一次 `free` 配对
第一部分是由我们完成的:当我们调用 `String::from` 时,其实现会请求所需的内存。这在编程语言中非常普遍。
不过,第二部分有所不同。在有 *垃圾回收器garbage collectorGC* 的语言中GC 会跟踪并清理不再使用的内存,我们不需要考虑这个问题。在大多数没有 GC 的语言中,我们有责任识别内存何时不再被使用,并调用代码显式释放内存,就像我们请求内存时一样。正确做到这一点,历来是编程中的难题。如果我们忘记了,就会浪费内存。如果过早释放,就会产生无效变量。如果我们做了两次,那也是一个错误。我们需要在 `allocate` 一次内存的同时,严格 `free` 一次内存。
Rust 采取了不同路径:一旦拥有内存的变量超出作用域,该内存就会自动退回。下面是清单 4-1 中,咱们作用域示例的一个使用了 `String`,而非字符串字面值的版本:
Rust 采取了不同路径:一旦拥有内存的变量超出作用域,该内存就会自动归还。下面是清单 4-1 中咱们作用域示例的一个版本,使用 `String` 而非字符串字面值:
```rust