什么是所有权?
所有权是一组规则,用于控制 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 如何知道何时
清理该数据,然后Stringtype 就是一个很好的例子。
我们将专注于String这与所有权有关。这些
aspects 也适用于其他复杂数据类型,无论它们是由
标准库或由您创建。我们将讨论String在第 8 章中有更深入的介绍。
我们已经看到了字符串字面量,其中字符串值被硬编码到我们的
程序。字符串字面量很方便,但它们并不适合每个
在这种情况下,我们可能需要使用 text。一个原因是他们
变。另一个是,当我们编写
我们的代码:例如,如果我们想获取用户输入并存储它怎么办?为
在这些情况下,Rust 有第二种字符串类型String.此类型管理
数据,因此能够存储一定数量的文本
在编译时我们不知道。您可以创建一个String从字符串
literal 使用from函数,如下所示:
#![allow(unused)] fn main() { let s = String::from("hello"); }
双冒号::operator 允许我们为此特定的from函数Stringtype 而不是使用某种名称,例如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 对于在编译时大小未知且其 在运行程序时,大小可能会发生变化。
使用Stringtype,以支持可变的、可增长的文本,
我们需要在堆上分配一定量的内存,在编译时是未知的,
以保存内容。这意味着:
- 必须在运行时从内存分配器请求内存。
- 我们需要一种方法,在完成
我们
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这Stringdata 被复制,这意味着我们将
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已经
失效
这解决了我们的问题!仅s2valid,当它超出范围时,它会
单独会释放内存,我们就完成了。
此外,这里暗示了一个设计选择: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)。如果类型实现Copytrait 中,使用它的变量不会移动,而是被简单地复制,
使它们在赋值给另一个变量后仍然有效。
Rust 不允许我们用Copy如果类型或其任何部分,
已实施Drop特性。如果类型需要发生一些特殊的事情
当值超出范围并且我们将Copyannotation 添加到该类型,
我们将收到编译时错误。要了解如何添加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/)为准