什么是所有权?

所有权是一组规则,用于控制 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() {main

作为所有权的第一个示例,我们将查看一些变量的范围。一个 scope 是项目在程序中对其有效的范围。以 以下变量:

#![allow(unused)]
fn main() {
let s = "hello";
}

该变量引用字符串文本,其中字符串的值为 硬编码到我们程序的文本中。该变量从 它被声明,直到当前范围结束。示例 4-1 显示了一个 程序,其中包含注释变量的有效位置。ss

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

字符串类型

为了说明所有权规则,我们需要一个更复杂的数据类型 比我们在 “数据类型” 部分中介绍的那些 第 3 章。前面介绍的类型是已知大小的,可以存储 在堆栈上,并在其范围结束时从堆栈中弹出,并且可以是 快速而简单地复制以创建新的独立实例(如果另一个 部分代码需要在不同的 scope 中使用相同的值。但我们希望 查看存储在堆上的数据,并探索 Rust 如何知道何时 清理该数据,类型就是一个很好的示例。String

我们将专注于与所有权相关的部分。这些 aspects 也适用于其他复杂数据类型,无论它们是由 标准库或由您创建。我们将在第 8 章中更深入地讨论。StringString

我们已经看到了字符串字面量,其中字符串值被硬编码到我们的 程序。字符串字面量很方便,但它们并不适合每个 在这种情况下,我们可能需要使用 text。一个原因是他们 变。另一个是,当我们编写 我们的代码:例如,如果我们想获取用户输入并存储它怎么办?为 在这些情况下,Rust 有第二种字符串类型 .此类型管理 数据,因此能够存储一定数量的文本 在编译时我们不知道。您可以从字符串创建 literal 使用函数,如下所示:StringStringfrom

#![allow(unused)]
fn main() {
let s = String::from("hello");
}

双冒号运算符允许我们在类型下命名这个特定的函数,而不是使用像 .我们将在 “Method Syntax“部分,当我们讨论时 关于“引用 Item in 中的 Item 的路径”中模块的命名空间 Module Tree“的 Tree。::fromStringstring_from

这种字符串可以改变:

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!`
}

那么,这里有什么区别呢?为什么可以 mutated 但 literals 不能?区别在于这两种类型如何处理内存。String

内存和分配

对于字符串字面量,我们在编译时知道内容,因此 text 直接硬编码到最终可执行文件中。这就是为什么字符串 文本快速高效。但这些属性仅来自字符串 literal 的不可变性。不幸的是,我们不能将内存 blob 放入 binary 对于在编译时大小未知且其 在运行程序时,大小可能会发生变化。

使用 type,为了支持一段可变的、可增长的文本, 我们需要在堆上分配一定量的内存,在编译时是未知的, 以保存内容。这意味着:String

  • 必须在运行时从内存分配器请求内存。
  • 我们需要一种方法,在完成 我们。String

第一部分由我们完成:当我们调用 时,它的实现 请求所需的内存。这在编程中几乎是通用的 语言。String::from

但是,第二部分不同。在具有垃圾回收器的语言中 (GC) 中,GC 会跟踪并清理未使用的内存 现在,我们不需要考虑它。在大多数没有 GC 的语言中, 我们有责任确定何时不再使用内存,并 调用 code 显式释放它,就像我们请求它一样。执行此作 正确地编程历来是一个困难的编程问题。如果我们忘记了, 我们会浪费内存。如果我们太早这样做,我们将得到一个无效的变量。如果 我们这样做了两次,这也是一个错误。我们需要将一个与 正好一个 .allocatefree

Rust 采取了不同的路径:一旦 变量超出范围。下面是我们的范围示例的一个版本 来自示例 4-1 使用 a 而不是字符串字面量: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
}

在一个自然的点上,我们可以归还我们需要的记忆 到分配器:when 超出范围。当变量退出 范围内,Rust 会为我们调用一个特殊的函数。这个函数叫做 drop,它是 的作者可以放置 返回内存的代码。Rust 在关闭时自动调用 大括号。StringsStringdrop

注意:在 C++ 中,这种在项的 生命周期有时称为资源获取即初始化 (RAII)。 如果您使用过 RAII,您将熟悉 Rust 中的函数 模式。drop

这种模式对 Rust 代码的编写方式有深远的影响。看起来 现在很简单,但代码的行为可能会出乎意料 当我们想要让多个变量使用数据时,情况复杂 我们在堆上分配了。现在让我们来探讨其中的一些情况。

与 Move 交互的变量和数据

在 Rust 中,多个变量可以以不同的方式与相同的数据交互。 让我们看一个例子,在示例 4-2 中使用整数。

fn main() {
    let x = 5;
    let y = x;
}
示例 4-2:将 variable 的整数值赋值给xy

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

现在让我们看看版本:String

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

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

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

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

图 4-1:内存中的 String 表示形式,其中包含绑定到 s1 的值 “hello”

length 是 的内容有多少内存(以字节为单位) 目前正在使用。容量是从分配器接收的内存总量(以字节为单位)。length 和 capacity 很重要,但在这种情况下则不重要,因此现在,忽略 能力。StringString

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

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

图 4-2:变量 s2 在内存中的表示形式,该变量具有 s1 的指针、长度和容量的副本

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

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

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

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

为了保证内存安全,在这一行之后,Rust 认为 为 不再有效。因此,Rust 在 go 超出范围。看看当你尝试使用 after is 时会发生什么 创建;它不会起作用:let s2 = s1;s1s1s1s2

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。在此示例中,我们会说 已移至 。因此,实际发生的情况如图 4-4 所示。s1s2

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

图 4-4:s1 之后内存中的表示 失效

这解决了我们的问题!使用 only valid,当它超出范围时,它会 单独会释放内存,我们就完成了。s2

此外,这里暗示了一个设计选择:Rust 永远不会 自动创建数据的“深层”副本。因此,可以假设任何自动复制在运行时性能方面都是廉价的。

范围和分配

对于范围界定、所有权和 内存也通过函数释放。当您将 new 值添加到现有变量中,Rust 将调用并释放原始的 值。例如,请考虑以下代码:dropdrop

fn main() {
    let mut s = String::from("hello");
    s = String::from("ahoy");

    println!("{s}, world!");
}

我们首先声明一个变量并将其绑定到值为 .然后我们立即创建一个值为 将其分配给 。此时,没有任何内容引用 堆。sString"hello"String"ahoy"s

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

图 4-5:初始 value 已被完全替换。

因此,原始字符串会立即超出范围。Rust 将在其上运行该函数,并且其内存将立即释放。当我们打印值 最后,它将是 。drop"ahoy, world!"

与 Clone 交互的变量和数据

如果我们确实想深度复制 的堆数据,而不仅仅是 stack data 中,我们可以使用一种名为 的常用方法。我们将讨论方法 语法,但是因为方法在许多 编程语言,您可能以前见过它们。Stringclone

下面是该方法的运行示例: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}");
}

但是这段代码似乎与我们刚刚学到的相矛盾:我们没有调用 ,但仍然有效并且没有被移动到 .clonexy

原因是在编译时具有已知大小的类型(例如整数) 时间完全存储在堆栈上,因此可以快速复制实际值 制作。这意味着我们没有理由想要阻止 在我们创建变量 .换句话说,没有区别 这里介于深复制和浅复制之间,所以跟注不会做任何事情 与通常的浅层复制不同,我们可以省略它。xyclone

Rust 有一个特殊的注解,叫做 trait,我们可以把它放在上面 类型,就像整数一样(我们将详细讨论 traits)。如果一个类型实现了 trait,则使用它的变量不会移动,而是被简单地复制。 使它们在赋值给另一个变量后仍然有效。CopyCopy

Rust 不允许我们使用 if 类型或其任何部分 已实现 trait。如果类型需要发生一些特殊的事情 当值超出范围并且我们将 Annotation 添加到该类型时, 我们将收到编译时错误。了解如何添加批注 到你的类型中实现 trait,请参阅“Derivable 性状”。CopyDropCopyCopy

那么,哪些类型实现了 trait 呢?您可以查看文档 当然,给定的类型,但作为一般规则,任何一组简单标量 values 可以实现 ,并且不需要分配或一些 形式的资源可以实现 。以下是一些类型 实现:CopyCopyCopyCopy

  • 所有整数类型,例如 .u32
  • 布尔类型 , ,具有值和 .booltruefalse
  • 所有浮点类型,例如 .f64
  • 字符类型 .char
  • Tuples 中,如果它们仅包含也实现 .例如,implements ,但不实施。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 注解的函数

如果我们尝试在调用 之后使用 ,Rust 会抛出一个 编译时错误。这些静态检查可以保护我们免受错误的影响。尝试添加 代码,并查看您可以在何处使用它们 所有权规则会阻止您执行此作。stakes_ownershipmainsx

返回值和范围

返回值还可以转移所有权。示例 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 超出范围,则该值将被 unless ownership 的数据已移动到另一个变量。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/)为准