借用检查器和内存管理介绍借用检查器

2025-06-07

借用检查器和内存管理

介绍

借贷检查器

介绍

我断断续续玩 Rust 大概有 12 个月了,其中一点就是我爱上了Rust。至于我是否愿意在日常工作中用 Rust 来做生产环境的工作,那就另当别论了(不过我觉得应该差不多了)。但我希望有一天能做到,真的非常希望。

那么,Rust 为何如此优秀,值得你花时间学习呢?最近,微软表示对它很感兴趣(参见此处),它在 AWS 和 Azure 的后台都有使用(AWSAzure 的链接分别参见此链接),它可以实现绿色线程(https://tokio.rs/),而且性能惊人(根据各种互联网资源和经验,其性能堪比 C/C++)。本质上,Rust 本身就值得写几篇文章来探讨,谷歌搜索“为什么开发者喜欢 Rust”。

那么,为什么我说我不能放弃 Java、JavaScript 或 Python 等高级语言呢?原因如下:

  • 说实话,入门很难。一个问题是,我们能否找到开发人员来开发和维护现有的 Rust,而不用因为人手稀缺而花费巨资?我希望随着时间的推移,这种情况会有所改善,但目前来说,找到 Java 或 JavaScript 开发人员可能更容易。
  • 在我看来,单元测试做得并不好。他们可能没有在这方面投入太多精力,因为他们的理念似乎是“如果能编译,就很可能能用”。说实话,即使这是真的(但事实并非如此,即使是 Rust 也不例外),我仍然希望进行正确的单元测试。单元测试并不可怕,它完全可以做到,事实上,它的基础相当扎实。但当我把它与 JavaScript 或 Python 进行比较时……它严重缺少优秀的模拟库。补充一下,我在写这篇文章的时候发现了这一点,也许它符合我的要求,但我还没有完全确认。
  • 对于云技术,我们目前还处于起步阶段。我还没怎么接触过,但谷歌一下就发现情况确实如此。虽然尝试在 AWS 或 Azure 上运行 Java/Node/Python 代码,但市面上有很多(通常由供应商开发)可用的库,但 Rust 尚未引起人们的足够关注。我再次希望并相信,在未来几年内,Rust 会受到更多关注。我注意到,现在我们可以用 Rust 编写无服务器代码,这很棒,所以 Rust 肯定会有所发展。
  • 绿色线程仍需改进。与 Go 相比,虽然我总体上更喜欢 Rust,但它提供了非常不错的绿色线程。在 Rust 中实现同样的事情更难。我有点希望 Rust 不要在运行时编码这种东西,Rust 可以使用 Tokio crate 来实现,这很好,但很难。我仍然推荐使用 Tokio,也有一些用它编写的严肃的东西,但对于胆小的人来说并非如此。我相信这会随着时间的推移而得到显著改善,甚至不一定需要那么久,而且这也不是一个公平的抱怨,因为核心团队现在才刚刚正确整理好 Futures 和 Async/Await,但我仍然想要它。然而,正常的多线程非常棒,这主要是因为这篇文章中的内容。

所以我一直在思考我能做些什么来解决上述问题。我的意思是,我想用这个工具,它太棒了。对于任何底层系统编程,我都会毫不犹豫地使用它,但我更想在日常构建云微服务时使用它。我决定,或许可以写一系列文章作为教程,介绍学习 Rust 过程中的难点,为大家提供一个起点。现在有很多关于这类文章的文章,我会在这里引用这个非常优秀的系列,但这个系列与 Haskell 的相似之处太多了,我不太喜欢。我不能保证我的教程真的更好,但我试图将它定位在稍微低一点的水平,理想情况下,我希望它的目标读者是那些没有编程经验的人,但按理说,没有一点经验不太可能。不过,我认为那些有一定编程基础的人可以从这些文章中有所收获。我假设读者需要大致了解某些语言(不一定是 Rust)中函数、模块、字符串、整数和内存的含义,但只要有基本的了解,阅读本文就足够了。

我认为 Rust 最初的主要障碍是借用检查器和内存管理,所以这将是本文的主题。这在很多方面都非常令人遗憾,因为借用检查器是 Rust 最棒的功能。我对 Rust 新手的建议是不要害怕它:它喜欢你,它希望你写出没有 bug 的优秀代码,它想要帮助你。虽然 Rust 的学习曲线陡峭且困难,但它提供了非常有用的编译时错误提示(我将在本文中尝试说服大家)。我曾与一些因为借用检查器而放弃 Rust 的人交谈过,如果这是真的,那么这篇文章就是为你准备的。读完这篇文章后,请再尝试一下,如果你还有什么不明白的,请之后再问我。

我还注意到,现在阅读 Rust 相关资料时,它比以前好多了。它仍然以极难使用而闻名,但 Rust 社区很棒,他们倾听问题,并努力改进,而且他们确实做到了(例如,参见这篇关于非词法生命周期的文章)。虽然绿色线程目前还没有达到我的预期,但我知道他们正在努力改进,我相信不久之后它就会好起来。

这将是一系列关于 Rust 难点的文章,但并非详尽无遗。我并非想取代《Rust》一书,也永远不会。《Rust》一书很棒,但它更适合作为参考,而非教程。我强烈建议你也尝试阅读《Rust》一书,你会需要它的。如果这篇文章让你感到困惑,你可以阅读《Rust》一书的前三章(比较简单的章节 :))。

Rust 虽然很难,但我保证它是可行的,我会尽力(当然是比喻)牵着你的手,指导你完成初始阶段。这篇文章没有什么高深莫测的东西,甚至没有那么难,但 Rust 有很多值得学习的地方,即使是经验丰富的程序员,也可能需要从头开始学习。这些文章应该能让你做好充分的准备,涵盖你所需的一切(反正它们都写出来了,但即使只有这篇文章,对经验丰富的程序员来说也足够了)。

不过,归根结底,如果你追求的是一门简单易用的编程语言,Rust 并不适合你。我相信 Rust 是当今低级 C/C++ 以及 Python 或 Java 等高级语言中复杂程序的有力竞争对手。如果你想要一个快速简单的程序和/或快速原型设计,Rust 并不适合你。

建议尝试运行以下程序并亲自体验。如果您不想安装 Rust,可以尝试Rust playground。此外,还会出现类似的编译器警告warning: unused variable: i,我不会允许这种情况出现在我正在编写的生产环境中,在教程中我也不想被这个问题困扰,但基本上如果我使用它,这个问题就会消失。它告诉我,如果我删除这个变量,程序不会改变,它又在试图帮助我。你应该始终修复编译器警告,始终,始终,始终。

最后,我并不假装自己是 Rust 专家,但我可能已经解决了足够多的基础问题,现在的水平比初学者略高一些。我很乐意听听各位 Rustaceans 的反馈,请私信我。

借贷检查器

本文主要讲述借用检查器。我们将在后续文章中进一步讨论,但现在我们需要先完全掌握它。一旦你理解了借用检查器,你就相当了解了 Rust,很多之前可能想不到的事情也会迎刃而解。我们也会介绍一些语法,但主要还是一篇理论文章,探讨学习 Rust 的最大障碍(在我看来)。

所有权

好的,我们开始吧。第一个话题是所有权,而且永远都是。重要的是,学习并消化它的工作原理。一旦你做到了,你就能用 Rust 做出很酷的东西了,我保证。

内存管理基本上是这里的主题。如果您之前用过 C 或 C++ 编程,您可能知道内存管理可能带来的麻烦。如果您用过高级语言编程,您可能不太了解,因为高级语言会帮您处理内存管理,但这是以垃圾收集器为代价的。

你可能从未关心过垃圾收集器,它会拖慢你的速度,而且它只能解决释放内存的问题。Rust 的功能更多,但我稍后会讲到。但垃圾收集器确实很棒,我更喜欢它,而不是 C/C++,因为 C/C++ 的内存管理会妨碍你执行操作。Rust 本质上提供了一种介于两者之间的机制,因此在很多方面都更胜一筹。

如果你以前没有用过 C/C++ 编程,那么如果你想以动态的方式预留内存(比如我现在需要一个包含 10 个元素的列表,但很快我会需要 12 个元素的列表,诸如此类),你需要预留内存并在完成后释放它。例如,要在堆上为整数预留内存(基本上是动态内存存储,你现在不需要担心这一点),我需要告诉 C++ 按照如下方式创建它(不用担心 C++ 语法,本文不需要它):

int* i = new int;
*i = 1;
Enter fullscreen mode Exit fullscreen mode

然后当我完成后我需要做:

delete i;
Enter fullscreen mode Exit fullscreen mode

如果我忘记了后者,或者重复执行了两次,那么就会出现问题,尤其是在后者的情况下。就这么简单,标准规定,如果我释放两次,基本上任何事情都可能发生。我在 Linux 上尝试释放了两次,但什么也没发生;在 Mac 上,我得到了以下结果(你的结果可能会有所不同):

➜  ./a.out
a.out(67865,0x1047a05c0) malloc: *** error for object 0x7fa4e8c02ac0: pointer being freed was not allocated
a.out(67865,0x1047a05c0) malloc: *** set a breakpoint in malloc_error_break to debug
[1]    67865 abort      ./a.out
Enter fullscreen mode Exit fullscreen mode

在 C++ 中,您需要考虑这里的不同类型的内存(熟悉的人会想到堆栈和堆),而在 Rust 中则不需要那么担心。堆栈和堆仍然存在,但老实说,一开始您不需要太担心。如果您深入了解 Rust,也许会担心,但老实说,我还不必关心。Rust 也没有 GC(垃圾收集),但它和有 GC 时一样安全,甚至更安全。我无法从 Rust 中得到上述运行时错误,我试过了(即使在不安全的 Rust 中,我们这里不涉及,我也得不到它。首先远离不安全的 Rust,那里有龙)。我得到了编译器错误,但没有运行时错误,当然编译器错误更好,因为我更快地得到了它们,而且我知道我一定破坏了一些东西。

原因与所有权有关,在 Rust 中,所有东西都必须被某个东西“拥有”。一旦它不再被某个东西拥有,内存就会被清除。这就像编译器帮delete我在 C++ 中添加了这条语句一样(这很酷,我不需要记住它)。但是,被某个东西“拥有”是什么意思呢?

让我们举一个非常简单的例子:

fn main() {
    let i: u32 = 32;
}
Enter fullscreen mode Exit fullscreen mode

这根本就是一个毫无意义的 Rust 程序。它声明一个变量i为 类型u32(无符号 32 位整数,Rust 的类型系统C/C++ 更健全),并将其值设置为32。Rust 的编译器会注意到,当该函数返回时,i不再需要,因此它会进行必要的清理(技术上讲,这在 C/C++ 中也有效,因为它位于堆栈中,但如果它在 Rust 中位于堆中,也同样有效)。本质上,您无需担心内存管理,Rust 已经为您处理好了。

类似地,我们可以考虑以下程序

fn main() {                        // Scope of `main` starts here
    let i: u32 = 32;
    {                              // A new inner scope starts here
        let j: u32 = i + 5;
        println!("j is: {}", j);
    }                              // The inner scope ends here and `j` is dropped
}                                  // The `main` scope ends here and `i` is dropped
Enter fullscreen mode Exit fullscreen mode

运行此程序会打印出这一行j is: 37。我们不会println详细解释该语句,但本质上它打印的是给定的字符串,并且每当{}字符串中有一个 时,我们就可以替换{}为一个变量值。这里, 被 的值替换了j

在这里,我们用大括号在中间创建了另一个作用域。如果您不熟悉其他 C 系列语言中这样的作用域,那么我来介绍一下,作用域基本上就是由这些花括号创建的,左花括号{和右花括号之间的任何内容}都是作用域。在这种情况下,我在main第 1 行到第 7 行之间创建了一个函数作用域,在第 3 行定义之后创建了一个内部作用域,i并在第 6 行结束。这个内部作用域中的所有内容都可以访问其上方作用域中的变量,但main作用域本身无法访问中间的内容。例如,内部作用域有和,ij作用域main只有i。在 Rust 中,像这样的内部作用域并不罕见(在这方面有点像 JavaScript)。我们可以用这样的作用域来限制我们需要变量的时间。您还可以通过在内部作用域中使用相同的名称来屏蔽变量,例如,请参见此处。

现在回到代码示例,和i之前一样,一旦 main 返回, 就会超出作用域。但是,现在我们通过内部括号有了另一个作用域。在这个作用域中,我们创建了一个新u32变量j,我们也可以访问i,因此我们可以将其设置ji + 5,或者实际上37。现在,一旦我们到达内部作用域的末尾,即程序经过第 6 行的右花括号后,我们就无法再访问 了j。例如,考虑以下程序:

fn main() {
    let i: u32 = 32;
    {
        let j: u32 = i + 5;
    }
    println!("j is: {}", j);
}
Enter fullscreen mode Exit fullscreen mode

当我们编译它时,我们得到:

➜ rustc simple_rust_program.rs
error[E0425]: cannot find value `j` in this scope
 --> simple_rust_program.rs:6:26
  |
6 |     println!("j is: {}", j);
  |                          ^ help: a local variable with a similar name exists: `i`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0425`.
Enter fullscreen mode Exit fullscreen mode

现在让我们花点时间看看这个错误输出有多精彩。它准确地告诉了我我需要知道的内容,但j第六行并不存在。它甚至告诉我一个命令来更完整地解释错误,它认为我可能指的是…… i。我确切地知道它在抱怨什么,它真的试图帮助我修复它。我真的很喜欢这个程序的借用检查器,它真的在尽力帮助我(我建议你记住这一点)。

我们已经讲完了所有权的含义,这很简单,对吧?这有什么大不了的?那我们继续讨论下一个话题:传递信息。

转让所有权

当然,有时我们想要传递变量,例如在函数调用中,我们可能想将变量作为参数传递。现在我们有三种方法(这三种方法在 Rust 中经常出现)来实现这一点:所有权转移、引用传递或可变引用传递。我们现在介绍所有权转移(另外两种将在下面的“借用”部分介绍)。

所有权转移在语法上非常简单,正如你所期望的那样。以下程序展示了所有权转移的过程:

fn take_ownership(a: String) {
    println!("I have a: {}", a);                 // and a is now owned by take_ownership
}

fn main() {
    let a: String = "Test String".to_string();   // a is owned by main
    take_ownership(a);                           // Now we pass a to take_ownership
    //println!("I try to print a: {}", a);
    // If I uncomment this line above then I get an error because this scope no longer owns a
}
Enter fullscreen mode Exit fullscreen mode

简单来说,它.to_string()会创建一个字符串,我们可以从字符串字面量中修改它。更多详情请参阅本书 8.2 节

所以,如果你像这样传递所有权,它就会从原始函数中消失,任何时候都只能有一个对象拥有一个变量。当然,除非你把它传回来。我可以修改上面的代码如下:

fn take_ownership(a: String) -> String {
    println!("I have a: {}", a);
    return a;
}

fn main() {
    let a: String = "Test String".to_string();
    let a = take_ownership(a);
    println!("a: {}", a);            // We've passed a back so we're OK now
}
Enter fullscreen mode Exit fullscreen mode

顺便说一句,上面的程序的返回方式不太像 Rust。等效地,这个函数可以写成:

fn take_ownership(a: String) -> String {
    println!("I have a: {}", a);
    a
}
Enter fullscreen mode Exit fullscreen mode

这完全是一回事。因为最后一行没有分号,Rust 知道这是一个回车符。关于语句和表达式的解释已经写过了,但我不想在这里赘述,如果你看到这个,请注意,这只是一个回车符。

够简单了吧?嗯,不完全是。有一个复杂的问题我们稍后会讨论,但现在我们先来讨论一下可变性。

可变性

接下来要讨论可变性,或者你可能更倾向于考虑非常量变量或可以修改的变量(或者干脆就叫变量吧,当然我可以修改一个变量,或者它不会改变,那就这样吧)。本质上,Rust 中的所有内容默认都是不可变的,也就是说,它是常量,其值不能被改变。如果我有以下程序:

fn main() {
    let i: u32 = 1;
    i = 2;
    println!("i is: {}", i);
}
Enter fullscreen mode Exit fullscreen mode

如果我尝试,我会得到类似以下内容:

error[E0384]: cannot assign twice to immutable variable `i`
 --> src/main.rs:3:5
  |
2 |     let i: u32 = 1;
  |         -
  |         |
  |         first assignment to `i`
  |         help: make this binding mutable: `mut i`
3 |     i = 2;
  |     ^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.
Enter fullscreen mode Exit fullscreen mode

问题出在哪里,是不是很清楚?但该怎么做呢?其实很简单,我们只需mut按照下面的代码添加关键字,就可以创建一个可变变量(可以更改的变量)。事实上,错误信息已经告诉了我具体该怎么做。

fn main() {
    let mut i: u32 = 1;        // Added mut, now I can change it
    i = 2;
    println!("i is: {}", i);
}
Enter fullscreen mode Exit fullscreen mode

有趣的是,我可以将不可变变量的所有权传递给可变变量,这完全没问题。例如:

fn take_ownership_and_pass_back(mut a: String) -> String {
    a = a + "2";
    a
}

fn main() {
    let a: String = "TEST".to_string();
    let a = take_ownership_and_pass_back(a);
    println!("a: {}", a);
}
Enter fullscreen mode Exit fullscreen mode

因此我将a不可变变量改为可变变量,这样就没问题了。如果我运行这个程序,我会得到一行a: TEST2。但是a是不可变的吗?Rust 不能保证你不会故意将其改为可变的,但你必须尝试去改变它。它不是试图告诉你该做什么,而是试图让你停止改变那些你从未想过要改变的东西。一般来说,不可变变量就是因为这个原因而很棒,这也是为什么它们是默认变量的原因。关键在于我可以限制某些东西需要可变的时间,从而限制可以改变值的地方(并且根据函数式编程创建错误,我们不希望这样)。

另一个有趣的代码片段如下:

fn main() {
    let mut s = "TEST1".to_string();
    let t = s;
    // Can't do the following as s is now moved
    //println!("s={}", s);
    s = "TEST2".to_string();
    println!("s={}, t={}", s, t);
}
Enter fullscreen mode Exit fullscreen mode

这里我们创建了一个s值为 的可变字符串"TEST1"。接下来,我们让t取得这个字符串的所有权,此时我无法再读取它,s因为它的值已被移到其他地方,并且为空。但是,s由于它是可变的,我可以重新赋值给 ,然后从中读取它,因为它现在已经初始化了。这完全没问题,正如上面的运行代码所示。我得到了输出s=TEST2, t=TEST1

这里还有另一个我们不允许做的常见问题,让我们看一下以下内容。

fn main() {
    let x = "TEST".to_string();
    for i in 0..10 {
        let y = x + &i.to_string();
        println!("{}", y);
    }
}
Enter fullscreen mode Exit fullscreen mode

这里我们尝试循环10次,并尝试将连接项"TEST"i整数合并成一个字符串。不幸的是,我们遇到了一个问题,你能在不编译的情况下找到原因吗?

如果我们尝试编译它,我们会得到以下错误:

error[E0382]: use of moved value: `x`
 --> src/main.rs:4:17
  |
2 |     let x = "TEST".to_string();
  |         - move occurs because `x` has type `std::string::String`, which does not implement the `Copy` trait
3 |     for i in 0..10 {
4 |         let y = x + &i.to_string();
  |                 ^ value moved here, in previous iteration of loop

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
Enter fullscreen mode Exit fullscreen mode

因为x在第一次循环中被移动,然后在 this 结束时超出了作用域,x所以在一次循环后被丢弃,因此我们在第二次循环中无法访问x。编译器识别出这一点并抛出上述错误。有很多方法可以解决这个问题,包括下面这个乍一看令人惊讶(但事后想想却很合理)的方法:

fn main() {
    let mut x = "TEST".to_string();
    for i in 0..10 {
        let y = x + &i.to_string();
        println!("{}", y);
        x = "TEST".to_string();
    }
}
Enter fullscreen mode Exit fullscreen mode

我们现在尝试通过复制和克隆来解决这类问题。使用后面的引用在这里也同样有效。

复制和克隆

我之前说过,你可以转移变量的所有权,并且演示了如何操作。我还提到了这里有一个小问题,现在就来解释一下。我们来看下面的例子:

fn main() {
    let i: u32 = 123;

    {
        let mut j = i;     // <-- If i didn't satisfy copy this would be a move

        j = 124;

        println!("Not finished and j is: {}", j);
    }

    println!("Finished and i is: {}", i);
    // ^^ And if i didn't satisfy copy and hence moved above I wouldn't be able to read i here
}
Enter fullscreen mode Exit fullscreen mode

按照我之前提到的逻辑,创建时i应该将这里移到j内部块中j,但是由于它已经消失了,我无法i在最后的 println 中访问它。然而,我运行它,它编译成功,并得到了:

Not finished and j is: 124
Finished and i is: 123
Enter fullscreen mode Exit fullscreen mode

因此,我看到我将 的值复制i到 中j,并没有像我想象的那样移动任何东西,我可以愉快地继续使用未更改的i。Rust 帮助了我,因为它知道u32满足该Copy特征。有关该特征的更多详细信息,请参见此处Copy,但我会简要解释一下这意味着什么。如果您遇到过其他语言的接口,那么特征有点像接口,现在请阅读“接口”而不是“特征”,这样就足够接近了。本质上,特征是可以得到满足的东西,然后事情可以基于此采取相应的行动。在这种情况下,任何满足Copy特征的东西,编译器都知道它的值是被复制而不是移动的。

另一方面,如果我将其更改i为字符串,s如以下代码所示:

fn main() {
    let s: String = "123".to_string();

    {
        let mut t = s;

        t = "124".to_string();

        println!("Not finished and t is: {}", t);
    }

    println!("Finished and s is: {}", s);
}
Enter fullscreen mode Exit fullscreen mode

然后我收到如下错误:

error[E0382]: borrow of moved value: `s`
  --> src/main.rs:12:39
   |
2  |     let s: String = "123".to_string();
   |         - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
...
5  |         let mut t = s;
   |                     - value moved here
...
12 |     println!("Finished and s is: {}", s);
   |                                       ^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
Enter fullscreen mode Exit fullscreen mode

再看看那令人惊叹的输出(gcc要不clang你也吃惊吧)。它准确地告诉了我我需要知道的内容:s它在第 2 行创建,在第 5 行移动到这里,但现在我试图在第 12 行使用它,却被禁止了。

错误很严重,但现在我该怎么做呢?嗯,你可以做几件事。如果你对副本满意,只需在.clone()更改 的值的行中添加t,如下所示:

        t = "124".to_string().clone();
Enter fullscreen mode Exit fullscreen mode

现在 Rust 会复制它,因为我告诉了它。任何被克隆的对象都需要满足这个Clone特性,大多数情况都倾向于满足,但并非总是如此。克隆意味着该变量值的整个内存都会被复制到其他地方的内存中,如果某个变量可能特别大,你可能不希望它满足这个特性,这样你就不会意外地这样做。

但基本上,这实际上意味着每当我们传递u32这样的变量时,它都会复制数据而不是移动它。因此,通过复制它,我得到了一个全新的变量,它与原始变量完全无关。Rust 并不是唯一这样做的语言,我见过一些这样做的语言,例如 Python。通常情况下,对于固定大小的小对象,默认情况下都是如此(主要是布尔值和数字)。如果您发现类似的东西在看起来不应该的情况下起作用,那么很可能只是 Rust 复制了它并试图帮助您。当然,如果发生这种情况,并且您更改了第二个变量,则第一个变量不会改变,这一点需要小心。

所以现在我已经涵盖了开始使用所有权所需的大部分知识。但除非程序非常简单(我告诉过你,在这种情况下不要费心使用 Rust),否则我们不会走得太远。有时我们只是想借用一些东西,现在就来看看。

借款

现在我们知道了如何移动和复制文件,但如果我不想移动或复制它怎么办?我想访问它,看看里面有什么,但它不属于我,或者我想更改一些不属于我的东西。那么,您有以下几种选择:

  • 如果我只想从中读取,我可以创建对它的“引用”,我可以拥有任意数量的引用(只要不存在下一个“可变引用”)。
  • 如果我也想更新它,我可以创建一个指向它的“可变引用”,但我只能拥有其中一个,仅此而已。如果我拥有一个“可变引用”,编译器会阻止我创建任何其他可变或不可变的“引用”(“引用”默认是不可变的,Rust 中的一切都是如此)。
  • 我可以创建指向它的“智能指针”。我将在以后的文章中讨论这个问题,这或许只是最后的努力,但有时是必要的。

那么,什么是引用和可变引用呢?其实,我传递的不是所有权,而是引用(如果你熟悉 C 语言,它有点像指针)。引用本质上保存着一个内存地址,指向原始变量的位置。当你获取a一个变量的引用时b,它的所有权a并没有被持有,而是指向b“你可以指向这里查看它a,或者,a如果它是一个可变引用,即使它a仍然属于我,你也可以指向它并修改它”。我们现在来看一个例子。

对于上面的代码示例,我非常希望能够在另一个作用域中更改它而不获取所有权,比如在另一个函数中。好吧,我可以这样做,而且效果很好:

fn main() {
    let mut s: String = "123".to_string();

    {
        let t = &mut s;

        *t = "124".to_string();

        println!("Not finished and t is: {}", t);
    }

    println!("Finished and s is: {}", s);
}
Enter fullscreen mode Exit fullscreen mode

如果我运行这个我就会得到我想要的:

Not finished and t is: 124
Finished and s is: 124
Enter fullscreen mode Exit fullscreen mode

那么我在这里做了什么?首先,我必须将原始字符串更改为可变的,因为我正在更改它,以前我不需要这样做,因为我从来不想更改原始字符串。我还t通过执行创建了一个可变引用let t = &mut s;。这&是我获取引用的语法,mut意味着我想要一个可变引用,一个我可以更改的东西。如果我刚才写了,let t = &s;那么它t只是一个引用,我只能从中读取。当我想对原始字符串执行读取或写入(如果它是可变的)时,我需要取消引用它,我通过写入来执行此操作*t*这里的表示此地址指向的值。如果我t = "124".to_string()按照您可能想到的方式编写,我会收到以下错误:

 --> src/main.rs:7:13
  |
7 |         t = "124".to_string();
  |             ^^^^^^^^^^^^^^^^^ expected mutable reference, found struct `std::string::String`
  |
  = note: expected type `&mut std::string::String`
             found type `std::string::String`
help: consider dereferencing here to assign to the mutable borrowed piece of memory
  |
7 |         *t = "124".to_string();
  |         ^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
Enter fullscreen mode Exit fullscreen mode

看完这篇文章后,我将不再滔滔不绝地谈论 Rust 的错误报告,但是看,它告诉我如何修复它。

所以我们现在从上面理解了关于解引用的概念,任何之前了解过 Rust 的人可能都会想“我大多数情况下都不会这么做”。没错,我刚才撒谎了,抱歉。但事实是,谢天谢地,Rust 很聪明。在非二义性的情况下,Rust 编译器知道我明确想要解引用 this 的地方,并会自动添加解引用运算符。所以我在这行代码中使用了*来解引用,因为正如我们上面看到的,Rust 不知道我不想直接赋值给。但在下一行我打印变量的地方,我可以看到我不需要解引用,这样做也没什么坏处,但如果我不解引用,编译器会知道“他显然是想这么做的”。事实上,据我所知,大多数 Rust 开发人员也不会在这一行代码中使用 。t*t = "124".to_string();t*

我之前说过,我只能接受一个可变引用,并且一旦接受,就不能再接受其他引用。让我们尝试在创建不可变引用之后再创建另一个引用,看看会发生什么。我们会得到类似以下的错误:

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
 --> src/main.rs:6:17
  |
5 |         let t = &mut s;
  |                 ------ mutable borrow occurs here
6 |         let u = &s;
  |                 ^^ immutable borrow occurs here
7 |
8 |         *t = "124".to_string();
  |         -- mutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
Enter fullscreen mode Exit fullscreen mode

这是因为,正如我之前暗示的那样,如果我在可变引用存在的情况下尝试创建任何其他引用,Rust 都会阻止我。原因在于强制执行安全的多线程编程,竞争条件并不好玩,而通过实施这些规则,它们在很大程度上可以消除。因此,如果一个线程拥有一个可能正在修改内容的可变引用,当我尝试在另一个线程中读取它时,可能会引发数据竞争条件。因此,Rust 绝对保证了安全性,它确保你不会在多线程环境中搞砸事情,但代价是你必须理解这类文章。一旦你理解了本文的内容,其余的理论就会容易得多。

由于我刚刚解释过的引用规则,通过创建内部作用域来限制作用域在 Rust 代码中很常见。一旦作用域发生变化,事情就会变得清晰,这可以很好地解决你遇到的一些问题。创建一个非常小的作用域来更改内容是一种常见的模式。

类似地,如果我在引用存在之后尝试创建一个可变引用(我必须使用它,否则 Rust 足够聪明,知道没有问题):

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:6:17
   |
5  |         let u = &s;
   |                 -- immutable borrow occurs here
6  |         let t = &mut s;
   |                 ^^^^^^ mutable borrow occurs here
...
11 |         println!("Not finished and u is: {}", u);
   |                                               - immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
Enter fullscreen mode Exit fullscreen mode

在我早期接触 Rust 的时候,我经常会思考:如果我引用了一个可变变量,而这个可变变量开始被修改,这还能保证代码的安全性吗?我当时应该测试一下,但结果却让我感到困惑。我的意思是,考虑下面的程序:

fn main() {
    let mut x = "123".to_string();
    let y = &x;
    x += "456";
    println!("{}", y);
}
Enter fullscreen mode Exit fullscreen mode

我不能x在引用之后再使用可变引用,因为那样很危险。但如果x仍然可以更改值,那这又有什么不同呢?这也会编译失败,因为 Rust 甚至会在这些引用存在时阻止所有者进行更改。我不确定这方面是否有很好的文档记录,我当然很久以前就不知道这一点,但这就是它确保安全多线程的方式。如果我编译上面的程序,我会得到:

error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let y = &x;
  |             -- immutable borrow occurs here
4 |     x += "456";
  |     ^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", y);
  |                    - immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
Enter fullscreen mode Exit fullscreen mode

类似地,下面的程序也会失败:

fn main() {
    let mut x = "123".to_string();
    let y = &mut x;
    x += "456";
    println!("{}", y);
}
Enter fullscreen mode Exit fullscreen mode

一旦我创建了可变引用,我就只能在它存在时用它来更改内容,即使是它的所有者也无法再更改它。这次编译器错误:

error[E0499]: cannot borrow `x` as mutable more than once at a time
 --> src/main.rs:4:5
  |
3 |     let y = &mut x;
  |             ------ first mutable borrow occurs here
4 |     x += "456";
  |     ^ second mutable borrow occurs here
5 |     println!("{}", y);
  |                    - first borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0499`.
Enter fullscreen mode Exit fullscreen mode

简而言之,Rust 将确保安全,正如我们所拥有的:

  • Rust 将确保如果多个对象可以读取一个变量,那么任何对象都无法改变该变量。
  • Rust 将确保如果某些东西能够改变一个变量(可变变量或可变引用),那么只有那一个东西能够从该变量中读取和写入。
  • 上述两种情况的反例是,Rust 是否足够智能,知道自己无论如何都是安全的(有时确实如此)。例如,之前我尝试在同一个作用域内同时使用一个引用和一个可变引用,直到我同时使用它们时才出现问题。
  • 这些检查都是在编译器级别强制执行的,本文中没有任何内容是在运行时执行的,因此如果您做了一些潜在的危险的事情,您会更快地知道。

这差不多涵盖了借用检查器的基础知识。很简单吧?当然还有更多内容,我会在本系列的后续文章中介绍。希望您现在可以理解本书的第十章,并且能够顺利读完第十章。在本系列的后续几篇文章中,我将尝试介绍结构体、选项、线程、智能指针、生命周期和闭包,以及如何使用借用检查器处理这些内容。敬请期待。

文章来源:https://dev.to/strottos/learn-rust-the-hard-bits-3d26
PREV
AWS API 架构
NEXT
使用 Next.js、TypeScript 和 Stripe 实现类型安全支付