关于Rust基本概念的再理解

2025/6/163576 字

引言

本文主要再次回忆一下 Rust 的生命周期和所有权机制。

生命周期

先简单说说生命周期。

今天看到个视频,说 Rust 管理内存的方式就是把栈上的引用堆内存的指针变量的生命周期与其引用的堆内存进行绑定,

也就是说当一个栈上的关于堆内存的引用变量被释放的时候,其引用的内存也会跟着被释放。

而我们知道栈上的内存是不可能出现内存泄漏问题的,这样通过把堆上的变量的生命周期与栈上的引用绑定,就解决了内存泄漏的问题。

这个解释虽然有些过于简单了,不过让我更加清晰地认识了 Rust 如何管理内存的思想。

所有权机制(误解版本)

接下来继续说所有权机制。

说所有权机制之前,让我先说一下我之前非常不理解的地方:

最开始,我认为所有权机制就是保证同一块内存区域不会由多个变量同时代表(注意我这里说的是代表,不是指向或者引用。这是有区别的,我说的意思就是不会有两个变量本身的地址相同。我找 AI 解惑,AI 最开始都没有理解我的意思)。

然后我仔细一想,不对啊,难道有什么语言会出现同一块内存区域被多个变量代表的情况吗?根本就不会啊,因为大家在把一个变量的值(注意我这里说的是变量本身的值)给另一个变量都是通过新建一块内存区域然后把值复制过来的方式的呀。

那 Rust 这个所有权机制不是招笑吗。

然后我又仔细一想,会不会Rust在把一个变量的值给另一个变量的时候,并没有开辟一块新的内存,而是让新的变量直接代替掉原内存。如果不考虑函数调用和返回值的传递的话(因为这种方式没法实现跨栈帧变量传值)效率应该会比传统的方式要高。

那么 Rust 是向我猜想的这样是为了效率引入的所有权机制吗?

rust
    let b = String::from("hello world!");
    println!("{:p}", &b);
  
    let a = b;
    println!("{:p}", &a);
plaintext
0x7ffe1c0fe838
0x7ffe1c0fe8b0

实验表明,并不是。

这让我进一步开始怀疑 Rust 这个所谓的所有权机制到底有没有用。

因为其它语言的传值像是A想要把一本书分享给B,然后A拿了一叠空白的纸,把书上的内容抄到纸上之后再把抄好的纸给B。

而在Rust里面,A不仅也要抄一遍书,还要把自己有的书给涂白再拆成一叠白纸。

比喻推演

现在,让我使用重新想出所有权机制的方式来思考所有权机制到底是为了什么而存在的。

通过上面的话,我刚刚想到一个不错的比喻,我先介绍一下我的比喻,然后从重新想出生命周期机制开始讲起。

比喻定义

我们把变量当作是人,每个人都有一定的寿命,并且它们都没有记忆。

(突然意识到除了全局变量以外所有变量绑定的值的地址都是在栈上,不要急着反对我说什么“还有在堆上的变量啊”,这么说就说明你把变量和变量绑定的值或者叫数据以及变量管理的数据搞混了。变量就只是符号,代表着某些内存中的数据[除了全局变量之外都在栈上,因为变量只能在函数中声明(别给我说什么类的变量不在函数中声明啊。类里面的所谓“成员变量”实际上在类被实例化之前根本也就是一些记号,不是有具体对应的内存的变量。而类也一定会在某个函数(除了全局的)中被实例化。而函数所使用的内存空间就是栈)])

然后把变量绑定的数据当作是这个人随身携带的一本书,书里面的内容可多可少,但是每个人只能拥有一本书。当一个人死亡时其携带的书也会消失(准确说是被回收成白纸了,这里这么说方便点,我不想我的想象中还有一堆莫名其妙的白纸)

然后把堆当作是一个巨大的带编号的书架人们可以在这里托管自己的书籍。由于书架很大,人们只能靠把自己所拥有的书架位置的编号记录在自己的书本里面来保持自己与书架中书籍的联系。当不需要托管的时候,书架中的书就会消失(实际是回收成白纸),这部分空间就可以被其它人用于托管。

生活在这里的人们唯一的需求就是与别人分享书籍,分享的方式就是分享者把自己书本中的内容抄写或者覆盖到被分享者的书上。

问题解决

这套机制看似美好,不过存在几个问题。

1.内存泄漏与解决方案

试着设想,当A在书架中托管了自己的书籍之后,突然去世了。我们会发现,A在书架存放书籍占用的空间永远无法被其他人所用。如果这种情况持续不断地发生,书架的空间终会消耗完毕。这个就对应着内存泄漏。

此时一个简单的解决方案很自然就诞生了,我们修改书架的托管方式,不是由托管人自己选择释放空间,而是当托管人去世之后,自动释放掉它在书架中占用的空间。这样内存泄漏的问题就不会发生了(前提是没有人是永生的)。这个就是生命周期的概念。

2.生命周期的遗留问题

上述方案看似完美,不过还有一部分不足。

试设想,A在书架中托管了自己的书籍,然后在自己随身携带的书中记下了对应书架位置的编号。此时,B想要共享A在书架中的书籍,B当然可以选择通过抄写的方式获取A在书架中的书籍,但是B想到一个更加聪明的办法,直接抄写A在随身书中的书架编号到自己的随身书中,节省了大量时间。

然后,意外还是发生了,A又突然去世了,由于实现了生命周期的方式,按理应该清理掉A在书架中的书,但是B阻止了这种情况,因为他的随身书里面还记录着这个书架的编号,不让清理。那么到底是要清理还是不清理呢,这是是个问题。

仔细思考,上述问题的根本原因就是没有确定书架中的书到底是谁的。

于是我们同时记录下书架各个被占用的格子的拥有着是谁,并且只允许格子的拥有者访问里面的内容,也允许格子的拥有者对格子的所有权进行转让。

这样,只需要格子的拥有者去世之后就清理格子,就没有问题了。

(还有一种解决方案是记录这个格子被几个人同时占用,python用的这种,看似也很棒,不过也有问题(循环引用)。不然python为什么还是要有gc呢,这里就不多说了)

3.所有权机制的遗留问题

所有权机制是很不错,不过也封死了像上述提到的B那样通过抄写编号共享书籍的方式,使得分享数据的效率大幅降低。

再次思考,于是我们提出了借用机制:

也就是说,即使某个格子只能被某个特定的人拥有,但是也允许其他人通过抄写编号的方式“借用”对应的书籍。不过也因为是借用,所有书籍是否被清理还是需要看所有者是否去世。

4.借用机制的遗留问题其一

再次思考,这样还是会发生B借用了A的书架书籍之后,A突然去世的情况。就会导致B得到的编号对应的是空书架。(悬垂指针问题)

这里的解决方案就比较科幻了,不太容易想到。

这里我们引入拉普拉斯妖,他可以提前知道每个人的寿命。这样只需要在B向A借用书籍之前,算出A和B的寿命,当B的寿命比A长时,就不允许B借用。不过,如果B愿意保证自己在A去世之前放弃生命,还是可以允许B借用的。(完整的生命周期机制,需要对代码进行复杂的分析)

5.借用机制的遗留问题其二

解决了悬垂指针的问题。还有竞态条件的问题没有解决。

也就是,如果除了B,还有C等多个人也借用了A的书,因为他们看到是书架里面的同一本书。可能就会出现B在阅读而C在同一页乱涂乱画或者两个人都在同一页乱涂乱画等非常混乱的情况。

解决方案就是对借用进行严格的分级,分别如下两种。

不可变借用:只能看书,不能修改。可变借用:可以看也可以改。

然后只要限制不可变借用和可变借用不能同时存在,以及最多同时存在一个可变借用即可。B和C一起看一本书还是OK的。


以上,就是Rust对应内存问题的解决方案。可以看到,我们只是想解决内存泄漏问题,没想到又定下了这么多规则用于解决衍生问题。

因此就显得Rust的规则非常复杂。不过这样理清了来龙去脉之后,就显得很清晰了。

一些小实验

为什么关于 println! 不会拿走所有权?

在最开始学了所有权机制并且知道函数传参会转移所有权之后。我尝试写出以下代码。

rust
    let a: String = String::from("hello world!");
    println!("{}", a);
    println!("{}", a);

本来我以为第二个 println! 会出现编译错误,结果却正常运行了。

plaintext
hello world!
hello world!

查了之后才知道原来 println! 接受的是引用类型,所以当然不会转移所有权了。

这里吐槽一下 Rust,自动引用和解引用虽然好用,但是太隐蔽了(希望出一个可以关掉自动引用和解引用的宏,便于初学者学习,老手开发的时候才开着)。

这对于初学者来说,在学到这两个概念之前,往往会非常迷惑,总感觉代码不是照着自己学习的方式在运行。

关于基本类型“没有所有权”这件事

先看代码和运行结果

rust
    let a: u32 = 5;
    println!("{}", a);
  
    let b: u32 = a;
    println!("{}", a);
plaintext
5
5

在把 a 的值(5)的所有权给 b 之后为什么我们还能使用 a(的引用)?

查了之后才知道,基本类型都实现了 Copy 特征,有了这个特征的类型在“赋值”的时候不会采用转移所有权的方式,而是采用深拷贝的方式传值。

对于没有涉及到堆,并且大小又不大的基本类型,当然是直接复制更方便了。

下面的实现可以证明 Copy 特征的用处。

没有 Copy 特征

rust
#[derive(Debug)]
struct MyInt32(i32);

fn main() {
    let a: MyInt32 = MyInt32(5);
    println!("{:?}", a);
  
    let b: MyInt32 = a;
    println!("{:?}", a);
  
}
plaintext
error[E0382]: borrow of moved value: `a`
  --> src/main.rs:10:22
   |
6  |     let a: MyInt32 = MyInt32(5);
   |         - move occurs because `a` has type `MyInt32`, which does not implement the `Copy` trait
...
9  |     let b: MyInt32 = a;
   |                      - value moved here
10 |     println!("{:?}", a);
   |                      ^ value borrowed here after move

有 Copy 特征

rust
#[derive(Copy, Clone, Debug)]
struct MyInt32(i32);

fn main() {
    let a: MyInt32 = MyInt32(5);
    println!("{:?}", a);
  
    let b: MyInt32 = a;
    println!("{:?}", a);
  
}
plaintext
MyInt32(5)
MyInt32(5)

match 是否会转移所有权?

直接说结论了,当你通过模式匹配把值从变量里面掏出来的时候,实际上就是进行了一次赋值操作。

如果掏的是有 Copy 特征的变量,不会转移所有权。反之就会。

情况一(没转移):

rust
#[derive(Debug)]
enum Color {
    Rgb(u8, u8, u8),  // 全为有Copy数据类型
}

fn main() {
    let b = Color::Rgb(6, 6, 6);
    match b {
        Color::Rgb(r, g, b) => println!("RGB: {}, {}, {}", r, g, b),
    }

    println!("{:?}", b);
}
plaintext
RGB: 6, 6, 6
Rgb(6, 6, 6)

情况二(转移了):

rust
#[derive(Debug)]
enum ColorWithData {
    Rgb(u8, u8, u8),  // 全为有Copy数据类型
    Named(String),    // 有无Copy数据类型
}

fn main() {
    let b = ColorWithData::Named(String::from("hello world!"));
    match b {
        ColorWithData::Rgb(r, g, b) => println!("RGB: {}, {}, {}", r, g, b),
        // -----------------👇这里会移动所有权!
        ColorWithData::Named(s) => println!("{}", s),
    }

    println!("{:?}", b);
}
plaintext
error[E0382]: borrow of partially moved value: `b`
  --> src/main.rs:15:22
   |
12 |         ColorWithData::Named(s) => println!("{}", s),
   |                              - value partially moved here
...
15 |     println!("{:?}", b);
   |                      ^ value borrowed here after partial move
   |
   = note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
   = 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: borrow this binding in the pattern to avoid moving the value
   |
12 |         ColorWithData::Named(ref s) => println!("{}", s),
   |                              +++

Rust的编译器非常友好,给了我们解决方案。就是在模式匹配的时候,把变量的类型改成 ref,相当于创建了一个借用 s。这样就不会移动所有权了。

rust
#[derive(Debug)]
enum ColorWithData {
    Rgb(u8, u8, u8),  // 全为有Copy数据类型
    Named(String),    // 有无Copy数据类型
}

fn main() {
    let b = ColorWithData::Named(String::from("hello world!"));
    match b {
        ColorWithData::Rgb(r, g, b) => println!("RGB: {}, {}, {}", r, g, b),
        // ---------------------👇这里不会移动所有权了😀
        ColorWithData::Named(ref s) => println!("{}", s),
    }

    println!("{:?}", b);
}
plaintext
hello world!
Named("hello world!")

💬 评论区

写评论

暂无评论,来发表第一条吧。

© 2026 Corn 的自留地