什么是所有权?

所有权是一组规则,用于控制 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
}
示例 4-1:变量及其有效范围

换句话说,这里有两个重要的时间点:

  • 什么时候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;
}
示例 4-2:分配 variable 的整数值xy

我们大概可以猜到这是做什么的:“bind the value5x;然后 Make 中值的副本x并将其绑定到y.”我们现在有两个变量xy,并且两者都等于5.这确实是正在发生的事情,因为整数 是具有已知固定大小的简单值,这两个5值被推送 到 stack 上。

现在让我们看看String版本:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
}

这看起来非常相似,因此我们可以假设它的工作方式是 same:也就是说,第二行将复制s1并绑定 它来s2.但事实并非如此。

请看一下图 4-1 看看发生了什么String在 涵盖。一个String由三个部分组成,如左侧所示:指向 保存字符串内容、长度和容量的内存。 这组数据存储在堆栈上。右侧是 heap 来保存内容。

两个表:第一个表包含 s1 在
stack 的 URL 中,由其长度 (5)、容量 (5) 和指向第一个
值。第二个表包含
字符串数据。

图 4-1:内存中的表示String持有 Value"hello"绑定到s1

长度是String是 目前正在使用。容量是String已从分配器收到。length 和 capacity 很重要,但在这种情况下则不重要,因此现在,忽略 能力。

当我们分配s1s2Stringdata 被复制,这意味着我们将 pointer、length 和 capacity。我们不会复制 指针引用的堆上的 data 的 data 的 SET 文件。换句话说,数据 内存中的表示如图 4-2 所示。

三个表:表 s1 和 s2 表示
stack 的 URL 中,并且都指向堆上的相同字符串数据。

图 4-2:变量在内存中的表示s2,该 API 具有s1

表示形式看起来不像图 4-3,而 memory 就是 看起来 Rust 也复制了堆数据。如果 Rust 这样做了,则 操作s2 = s1如果 堆上的数据很大。

四个表:两个表,分别表示 s1 和 s2 的堆栈数据。
并且每个都指向堆上自己的字符串数据副本。

图 4-3:另一种可能性s2 = s1可能 如果 Rust 也复制了堆数据,则执行

前面我们说过,当一个变量超出范围时,Rust 会自动 调用drop函数并清理该变量的堆内存。但 图 4-2 显示了指向同一位置的两个数据指针。这是一个 问题:当s2s1超出范围,它们都会尝试释放 相同的内存。这称为双重释放错误,是内存 我们之前提到的安全错误。释放内存两次可能会导致内存 损坏,这可能会导致安全漏洞。

为保证内存安全,行后let s2 = s1;中,Rust 认为s1如 不再有效。因此,Rust 不需要在s1去 超出范围。看看当您尝试使用s1s2是 创建;它不会起作用:

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 所示。

三个表:表 s1 和 s2 表示
stack 的 URL 中,并且都指向堆上的相同字符串数据。
表 s1 灰显,因为 s1 不再有效;只有 S2 可用于
访问堆数据。

图 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.此时,没有任何内容引用 堆。

一个表 s 表示堆栈上的字符串值,指向
堆上的第二段字符串数据 (ahoy),带有原始字符串
数据 (hello) 灰显,因为它无法再访问。

图 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}");
}

但是这段代码似乎与我们刚刚学到的相矛盾:我们没有调用clonex仍然有效且未移至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,其中包含值truefalse.
  • 所有浮点类型,例如f64.
  • 字符类型char.
  • Tuples,如果它们仅包含也实现Copy.例如(i32, i32)实现Copy(i32, String)不。

所有权和功能

将值传递给函数的机制类似于 为变量赋值。将变量传递给函数将移动或 复制,就像 assignment 一样。示例 4-3 有一个带有一些注释的示例 显示变量进入和超出范围的位置。

文件名: src/main.rs
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.
示例 4-3:带有 owner 和 scope 注解的函数

如果我们尝试使用s调用takes_ownership,Rust 会抛出一个 编译时错误。这些静态检查可以保护我们免受错误的影响。尝试添加 code 添加到main使用sx以查看您可以在哪些位置使用它们以及在何处使用它们 所有权规则会阻止您执行此作。

返回值和范围

返回值还可以转移所有权。示例 4-4 显示了一个 函数返回一些值,其注释与 清单 中的注释类似 4-3.

文件名: src/main.rs
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
}
示例 4-4:转移返回值的所有权

变量的所有权每次都遵循相同的模式:分配一个 value 添加到另一个变量中会移动它。当包含 heap 超出范围,该值将被drop除非所有权 的数据已移动到另一个变量。

虽然这有效,但获取所有权,然后返回每个 功能有点乏味。如果我们想让一个函数使用一个值,但 不拥有所有权?很烦人的是我们传入的任何东西也需要 如果我们想再次使用它,则将其传回去,此外还会产生任何数据 从我们可能也想要返回的函数的主体中。

Rust 确实允许我们使用元组返回多个值,如示例 4-5 所示。

文件名: src/main.rs
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)
}
示例 4-5:返回参数的所有权

但对于一个本应如此的概念来说,这太过仪式化和大量工作 常见。幸运的是,Rust 有一个功能,可以在没有 转让所有权,称为引用

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准