什么是所有权?
所有权是一组规则,用于控制 Rust 程序如何管理内存。 所有程序都必须管理它们在运行时使用计算机内存的方式。 某些语言具有垃圾回收功能,会定期查找 no-longer used 程序运行时的内存;在其他语言中,程序员必须显式地 分配并释放内存。Rust 使用第三种方法:内存被管理 通过具有编译器检查的一组规则的所有权系统。如果 违反任何规则,程序将无法编译。没有任何功能 的所有权会减慢程序运行的速度。
因为所有权对许多程序员来说是一个新概念,所以它确实需要一些时间 来适应。好消息是,您对 Rust 的经验就越丰富 而所有权制度的规则,你自然会更容易发现它 开发安全高效的代码。坚持下去!
当您了解所有权时,您将拥有坚实的理解基础 使 Rust 独一无二的功能。在本章中,您将通过以下方式学习所有权 通过一些侧重于非常常见的数据结构的示例: 字符串。
堆栈和堆
许多编程语言不需要您考虑堆栈和 堆。但是在像 Rust 这样的系统编程语言中,无论 value 在堆栈上还是堆上会影响语言的行为方式和原因 你必须做出某些决定。部分所有权将在 与堆栈和堆的关系,所以这里有一个简短的 准备中的解释。
堆栈和堆都是可供代码使用的内存部分 在运行时,但它们的结构方式不同。堆栈存储 值 (values ) 按其获取顺序排列并删除相反的值 次序。这称为后进先出。想想一堆 盘子:当您添加更多盘子时,您将它们放在堆的顶部,而当 你需要一个盘子,你从顶部取下一个。添加或移除板 中间或底部也不起作用!添加数据称为推送 添加到堆栈上,删除数据称为 popping off the stack。都 存储在堆栈上的数据必须具有已知的固定大小。未知数据 size 或可能更改的大小必须存储在堆上 相反。
堆的组织性较差:当您将数据放在堆上时,您请求一个 一定的空间。内存分配器在堆中找到一个空位 ,将其标记为正在使用,并返回一个指针,该指针 是该位置的地址。此过程称为 在 heap 中,有时缩写为 just alassigning (将值推送到 堆栈不被视为分配)。因为指向堆的指针是一个 已知的固定大小,则可以将指针存储在堆栈上,但是当您需要 实际数据,您必须按照指针进行作。想想你坐在 餐厅。当您输入时,您需要说明小组中的人数,并且 主持人找到一张适合所有人的空桌子,并带你去那里。如果 您的小组中有人迟到,他们可以询问您坐在哪里 找到您。
推送到堆栈比在堆上分配更快,因为 allocator 永远不必搜索存储新数据的地方;该位置是 始终位于堆栈的顶部。相比之下,在堆上分配空间 需要更多的工作,因为分配器必须首先找到足够大的空间 保存数据,然后执行记账,为下一个 分配。
访问堆中的数据比访问堆栈上的数据慢,因为 您必须按照指针才能到达那里。现代处理器更快 如果他们在内存中跳来跳去的次数较少。继续这个类比,考虑一个服务器 在一家餐厅接受许多桌子的订单。获取 在进入下一桌之前,所有订单都在一张桌子上。采用 从表 A 订购,然后从表 B 订购订单,然后再次从 A 订购 1 订单,然后 然后再次来自 B 的 1 个将是一个慢得多的过程。同样,一个 如果处理器处理与其他数据接近的数据,它可以更好地完成工作 data(就像它在堆栈上一样)而不是更远的距离(因为它可以在 堆)。
当您的代码调用函数时,传递给函数的值 (可能包括指向堆上数据的指针)和函数的 局部变量被推送到堆栈上。当函数结束时,那些 值从堆栈中弹出。
跟踪代码的哪些部分正在使用堆上的哪些数据, 最大限度地减少堆上的重复数据量,并清理未使用的数据 数据在堆上,这样您就不会用完空间都是所有权的问题 地址。了解所有权后,无需考虑 stack 和 heap 中,但知道所有权的主要目的 是管理堆数据,可以帮助解释为什么它以这种方式工作。
所有权规则
首先,我们来看一下所有权规则。请牢记这些规则,因为我们 通过示例来说明它们:
- Rust 中的每个值都有一个所有者。
- 一次只能有一个所有者。
- 当所有者超出范围时,该值将被删除。
变量范围
现在我们已经超越了基本的 Rust 语法,我们不会包含所有fn main() {
代码,因此,如果你正在跟随,请确保将以下内容
examples 中main
功能。因此,我们的示例将是一个
更简洁一点,让我们专注于实际细节,而不是
样板代码。
作为所有权的第一个示例,我们将查看一些变量的范围。一个 scope 是项目在程序中对其有效的范围。以 以下变量:
#![allow(unused)] fn main() { let s = "hello"; }
变量s
引用字符串文本,其中字符串的值为
硬编码到我们程序的文本中。该变量从
它被声明,直到当前范围结束。示例 4-1 显示了一个
程序,并带有注释注释变量s
将有效。
fn main() { { // s is not valid here, it’s not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid }
换句话说,这里有两个重要的时间点:
- 什么时候
s
进入范围,则它是有效的。 - 在超出范围之前,它将保持有效。
此时,范围与变量何时有效之间的关系为
与其他编程语言类似。现在,我们将在此基础上进行构建
通过引入String
类型。
这String
类型
为了说明所有权规则,我们需要一个更复杂的数据类型
比我们在 “数据类型” 部分中介绍的那些
第 3 章。前面介绍的类型是已知大小的,可以存储
在堆栈上,并在其范围结束时从堆栈中弹出,并且可以是
快速而简单地复制以创建新的独立实例(如果另一个
部分代码需要在不同的 scope 中使用相同的值。但我们希望
查看存储在堆上的数据,并探索 Rust 如何知道何时
清理该数据,然后String
type 就是一个很好的例子。
我们将专注于String
这与所有权有关。这些
aspects 也适用于其他复杂数据类型,无论它们是由
标准库或由您创建。我们将讨论String
在第 8 章中有更深入的介绍。
我们已经看到了字符串字面量,其中字符串值被硬编码到我们的
程序。字符串字面量很方便,但它们并不适合每个
在这种情况下,我们可能需要使用 text。一个原因是他们
变。另一个是,当我们编写
我们的代码:例如,如果我们想获取用户输入并存储它怎么办?为
在这些情况下,Rust 有第二种字符串类型String
.此类型管理
数据,因此能够存储一定数量的文本
在编译时我们不知道。您可以创建一个String
从字符串
literal 使用from
函数,如下所示:
#![allow(unused)] fn main() { let s = String::from("hello"); }
双冒号::
operator 允许我们为此特定的from
函数String
type 而不是使用某种名称,例如string_from
.我们将在 “Method
Syntax“部分,当我们讨论时
关于“引用 Item in 中的 Item 的路径”中模块的命名空间
Module Tree“的 Tree。
这种字符串可以改变:
fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{s}"); // This will print `hello, world!` }
那么,这里有什么区别呢?为什么可以String
被 mutated 但 literals
不能?区别在于这两种类型如何处理内存。
内存和分配
对于字符串字面量,我们在编译时知道内容,因此 text 直接硬编码到最终可执行文件中。这就是为什么字符串 文本快速高效。但这些属性仅来自字符串 literal 的不可变性。不幸的是,我们不能将内存 blob 放入 binary 对于在编译时大小未知且其 在运行程序时,大小可能会发生变化。
使用String
type,以支持可变的、可增长的文本,
我们需要在堆上分配一定量的内存,在编译时是未知的,
以保存内容。这意味着:
- 必须在运行时从内存分配器请求内存。
- 我们需要一种方法,在完成
我们
String
.
第一部分由我们完成:当我们调用String::from
、其实现
请求所需的内存。这在编程中几乎是通用的
语言。
但是,第二部分不同。在具有垃圾回收器的语言中
(GC) 中,GC 会跟踪并清理未使用的内存
现在,我们不需要考虑它。在大多数没有 GC 的语言中,
我们有责任确定何时不再使用内存,并
调用 code 显式释放它,就像我们请求它一样。执行此作
正确地编程历来是一个困难的编程问题。如果我们忘记了,
我们会浪费内存。如果我们太早这样做,我们将得到一个无效的变量。如果
我们这样做了两次,这也是一个错误。我们需要只配对一个allocate
跟
正好一个free
.
Rust 采取了不同的路径:一旦
变量超出范围。下面是我们的范围示例的一个版本
从示例 4-1 中,使用String
而不是字符串文本:
fn main() { { let s = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no // longer valid }
有一个自然的点,我们可以将String
需要
分配给分配器:当s
超出范围。当变量退出
范围内,Rust 会为我们调用一个特殊的函数。此函数称为drop
,这是String
可以把
返回内存的代码。Rust 调用drop
在成交时自动
大括号。
注意:在 C++ 中,这种在项的
生命周期有时称为资源获取即初始化 (RAII)。
这drop
如果你用过 RAII,你就会很熟悉
模式。
这种模式对 Rust 代码的编写方式有深远的影响。看起来 现在很简单,但代码的行为可能会出乎意料 当我们想要让多个变量使用数据时,情况复杂 我们在堆上分配了。现在让我们来探讨其中的一些情况。
与 Move 交互的变量和数据
在 Rust 中,多个变量可以以不同的方式与相同的数据交互。 让我们看一个例子,在示例 4-2 中使用整数。
fn main() { let x = 5; let y = x; }
x
自y
我们大概可以猜到这是做什么的:“bind the value5
自x
;然后 Make
中值的副本x
并将其绑定到y
.”我们现在有两个变量x
和y
,并且两者都等于5
.这确实是正在发生的事情,因为整数
是具有已知固定大小的简单值,这两个5
值被推送
到 stack 上。
现在让我们看看String
版本:
fn main() { let s1 = String::from("hello"); let s2 = s1; }
这看起来非常相似,因此我们可以假设它的工作方式是
same:也就是说,第二行将复制s1
并绑定
它来s2
.但事实并非如此。
请看一下图 4-1 看看发生了什么String
在
涵盖。一个String
由三个部分组成,如左侧所示:指向
保存字符串内容、长度和容量的内存。
这组数据存储在堆栈上。右侧是
heap 来保存内容。
图 4-1:内存中的表示String
持有 Value"hello"
绑定到s1
长度是String
是
目前正在使用。容量是String
已从分配器收到。length 和
capacity 很重要,但在这种情况下则不重要,因此现在,忽略
能力。
当我们分配s1
自s2
这String
data 被复制,这意味着我们将
pointer、length 和 capacity。我们不会复制
指针引用的堆上的 data 的 data 的 SET 文件。换句话说,数据
内存中的表示如图 4-2 所示。
图 4-2:变量在内存中的表示s2
,该 API 具有s1
表示形式看起来不像图 4-3,而 memory 就是
看起来 Rust 也复制了堆数据。如果 Rust 这样做了,则
操作s2 = s1
如果
堆上的数据很大。
图 4-3:另一种可能性s2 = s1
可能
如果 Rust 也复制了堆数据,则执行
前面我们说过,当一个变量超出范围时,Rust 会自动
调用drop
函数并清理该变量的堆内存。但
图 4-2 显示了指向同一位置的两个数据指针。这是一个
问题:当s2
和s1
超出范围,它们都会尝试释放
相同的内存。这称为双重释放错误,是内存
我们之前提到的安全错误。释放内存两次可能会导致内存
损坏,这可能会导致安全漏洞。
为保证内存安全,行后let s2 = s1;
中,Rust 认为s1
如
不再有效。因此,Rust 不需要在s1
去
超出范围。看看当您尝试使用s1
后s2
是
创建;它不会起作用:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
你会收到这样的错误,因为 Rust 阻止你使用 无效的引用:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:15
|
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}, world!");
| ^^^^ value borrowed here after move
|
= 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`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
如果您在使用
其他语言、复制指针的概念、长度和容量
不复制数据可能听起来像是做一个浅拷贝。但
因为 Rust 也会使第一个变量无效,而不是被称为
浅拷贝,这被称为 move。在此示例中,我们会说s1
已移至s2
.因此,实际发生的情况如图 4-4 所示。
图 4-4:之后在内存中的表示s1
已经
失效
这解决了我们的问题!仅s2
valid,当它超出范围时,它会
单独会释放内存,我们就完成了。
此外,这里暗示了一个设计选择:Rust 永远不会 自动创建数据的“深层”副本。因此,可以假设任何自动复制在运行时性能方面都是廉价的。
范围和分配
对于范围界定、所有权和
通过drop
功能也是如此。当您将
new 值添加到现有变量中,Rust 将调用drop
并释放原始
值。例如,请考虑以下代码:
fn main() { let mut s = String::from("hello"); s = String::from("ahoy"); println!("{s}, world!"); }
我们首先声明一个变量s
并将其绑定到String
使用值"hello"
.然后我们立即创建一个新的String
使用值"ahoy"
和
将其分配给s
.此时,没有任何内容引用
堆。
图 4-5:初始 value 已被完全替换。
因此,原始字符串会立即超出范围。Rust 将运行drop
函数,并且其内存将立即释放。当我们打印值
最后,它将是"ahoy, world!"
.
与 Clone 交互的变量和数据
如果我们确实想深度复制String
,而不仅仅是
stack data 中,我们可以使用一个名为clone
.我们将讨论方法
语法,但是因为方法在许多
编程语言,您可能以前见过它们。
下面是clone
方法的实际应用:
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
这工作得很好,并显式地产生了如图 4-3 所示的行为, 其中,堆数据确实被复制。
当您看到对clone
,您就知道一些任意代码正在
执行,并且该代码可能很昂贵。这是一个视觉指示器,表明某些
不同的是正在发生。
仅堆栈数据:复制
还有另一个我们还没有讨论的皱纹。此代码使用 整数(其中一部分如示例 4-2 所示)有效且有效:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); }
但是这段代码似乎与我们刚刚学到的相矛盾:我们没有调用clone
但x
仍然有效且未移至y
.
原因是在编译时具有已知大小的类型(例如整数)
时间完全存储在堆栈上,因此可以快速复制实际值
制作。这意味着我们没有理由想要阻止x
从
在我们创建变量后生效y
.换句话说,没有区别
在深部和浅层之间复制,所以调用clone
什么都不做
与通常的浅层复制不同,我们可以省略它。
Rust 有一个特殊的注解,称为Copy
我们可以放置的 trait
类型,就像整数一样(我们将详细讨论
traits)。如果类型实现Copy
trait 中,使用它的变量不会移动,而是被简单地复制,
使它们在赋值给另一个变量后仍然有效。
Rust 不允许我们用Copy
如果类型或其任何部分,
已实施Drop
特性。如果类型需要发生一些特殊的事情
当值超出范围并且我们将Copy
annotation 添加到该类型,
我们将收到编译时错误。要了解如何添加Copy
注解
到你的类型中实现 trait,请参阅“Derivable
性状”。
那么,哪些类型实现了Copy
特性?您可以查看文档
当然,给定的类型,但作为一般规则,任何一组简单标量
values 可以实现Copy
,并且没有需要 allocation 或 some
形式可以实现Copy
.以下是一些类型
实现Copy
:
- 所有整数类型,例如
u32
. - 布尔型
bool
,其中包含值true
和false
. - 所有浮点类型,例如
f64
. - 字符类型
char
. - Tuples,如果它们仅包含也实现
Copy
.例如(i32, i32)
实现Copy
但(i32, String)
不。
所有权和功能
将值传递给函数的机制类似于 为变量赋值。将变量传递给函数将移动或 复制,就像 assignment 一样。示例 4-3 有一个带有一些注释的示例 显示变量进入和超出范围的位置。
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
如果我们尝试使用s
调用takes_ownership
,Rust 会抛出一个
编译时错误。这些静态检查可以保护我们免受错误的影响。尝试添加
code 添加到main
使用s
和x
以查看您可以在哪些位置使用它们以及在何处使用它们
所有权规则会阻止您执行此作。
返回值和范围
返回值还可以转移所有权。示例 4-4 显示了一个 函数返回一些值,其注释与 清单 中的注释类似 4-3.
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
变量的所有权每次都遵循相同的模式:分配一个
value 添加到另一个变量中会移动它。当包含
heap 超出范围,该值将被drop
除非所有权
的数据已移动到另一个变量。
虽然这有效,但获取所有权,然后返回每个 功能有点乏味。如果我们想让一个函数使用一个值,但 不拥有所有权?很烦人的是我们传入的任何东西也需要 如果我们想再次使用它,则将其传回去,此外还会产生任何数据 从我们可能也想要返回的函数的主体中。
Rust 确实允许我们使用元组返回多个值,如示例 4-5 所示。
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
但对于一个本应如此的概念来说,这太过仪式化和大量工作 常见。幸运的是,Rust 有一个功能,可以在没有 转让所有权,称为引用。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准