前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第4章 | 所有权

第4章 | 所有权

作者头像
草帽lufei
发布2024-05-08 15:21:12
570
发布2024-05-08 15:21:12
举报
文章被收录于专栏:程序语言交流程序语言交流
谈及内存管理,我们希望编程语言能具备两个特点:
  • 希望内存能在我们选定的时机及时释放,这使我们能控制程序的内存消耗;
  • 在对象被释放后,我们绝不希望继续使用指向它的指针,这是未定义行为,会导致崩溃和安全漏洞。

笔记 在前端知识点中很少涉及内存管理相关的概念,日常的业务开发不涉及底层,导致现在现在业务型选手,Rust中经常提到底层的知识,慢慢对JavaScript又有了新的理解

但上述情景似乎难以兼顾:只要指向值的指针仍然存在,释放这个值就必然会让这些指针悬空。几乎所有主流编程语言都只能在两个阵营中“二选一”,这取决于它们从中放弃了哪一项。

  • “安全优先”阵营会通过垃圾回收机制来管理内存,在所有指向对象的可达指针都消失后,自动释放对象。它通过简单地保留对象,直到再也没有指向它们的指针为止,来消除悬空指针。几乎所有现代语言都属于这个阵营,从 Python、JavaScript 和 Ruby 到 Java、C# 和 Haskell。 但是依赖垃圾回收,就意味着放弃了对于释放对象时机的精准控制,完全委托给回收器代管。一般来说,垃圾回收器就像奇怪的野兽般难以捉摸,要理解内存为何没有在预期的时机被释放可能颇具挑战。
  • “控制优先”阵营会让你自己负责释放内存。程序的内存消耗完全掌握在你的手中,但避免悬空指针也完全成了你的责任。C 和 C++ 是这个阵营中仅有的两种主流语言。 如果你永不犯错,这当然是很好的选择,但事实证明,只要是人就会犯错。从已收集的安全报告数据来看,指针滥用一直都是引发问题的罪魁祸首。

Rust 的目标是既安全又高效,所以这两种妥协都是无法接受的。但如果很容易两者兼得,那应该早就有人做到了。看来我们需要做一些根本性的变革。

Rust 通过限制程序使用指针的方式出人意料地打破了这种困局。本章和第 5 章将专门解释这些限制是什么以及它们为什么能起作用。现在,只需要知道一些惯用的常见结构可能不符合这些限制规则,而你要寻找替代方案。施加这些限制的最终目的是在混沌中建立足够的秩序,以便让 Rust 的编译期检查器有能力验证程序中是否存在内存安全错误:悬空指针、重复释放、使用未初始化的内存等。在运行期,指针仅仅是内存中的地址,和在 C 与 C++ 中一样。而不一样的是,Rust 编译器已然证明你的代码在安全地使用它们。

笔记 Rust 通过限制程序使用指针的方式。感叹,这个思路太棒了

这些规则同样构成了 Rust 支持安全并发编程的基础。使用 Rust 精心设计的线程原语,那些确保代码在正确使用内存的规则,同样可以用于证明代码中不存在数据竞争。Rust 程序中的缺陷不会导致一个线程破坏另一个线程的数据,进而在系统中的无关部分引入难以重现的故障。多线程代码中的固有不确定性被隔离到了那些专门设计来处理它们的线程特性(比如互斥锁、消息通道、原子值等)上,而不必出现在普通的内存引用中。用 C 和 C++ 编写的多线程代码饱受诟病,但 Rust 很好地改变了这种局面。

Rust 的激进“赌注”是其成功的基础和语言的根源。即使有这种限制,该语言依然足够灵活,可以完成几乎所有任务,并且可以消除各种内存管理和并发错误。这些优点将会证明你值得调整自己的风格来适应它。正是因为我们(本书作者)在 C 和 C++ 方面拥有丰富的经验,所以才更加看好 Rust。对我们来说,与 Rust 的这项交易非常划算。

Rust 的一些规则可能与你在其他编程语言中看到的截然不同。在我们看来,学习 Rust 的核心挑战,就是学习如何用好这些规则并转化为你的优势。在本章中,我们将首先展示同一个根本问题在不同语言中的表现形式,以深入了解 Rust 规则背后的逻辑和意图。然后,我们将详细解释 Rust 的规则,看看所有权在概念层和实现层分别意味着什么、如何在各种场景中跟踪所有权的变化,以及在哪些情况下要改变或打破其中的一些规则,以提供更大的灵活性。

4.1 所有权

如果你读过大量 C 或 C++ 代码,可能遇到过这样的注释,即某个类的实例拥有它指向的某个其他对象。通常,拥有对象意味着可以决定何时释放此对象:当销毁拥有者时,它拥有的对象也会随之销毁。

假如有如下 C++ 代码:

代码语言:javascript
复制
std::string s = "frayed knot";

通常,字符串 s 在内存中的表示如图 4-1 所示。

图 4-1:栈上的 C++ std::string 值,指向其在堆上分配的缓冲区

在这里,实际的 std::string 对象本身总是正好有 3 个机器字长,包括指向分配在堆上的缓冲区的指针、缓冲区的总容量(在不得不为字符串分配更大的缓冲区之前,文本可以增长到多大),以及当前持有的文本的长度。这些都是 std::string 类私有的字段,使用者无法访问。

std::string 拥有自己的缓冲区:当程序销毁字符串时,字符串的析构函数会释放缓冲区。以前,一些 C++ 库会在多个 std::string 值之间共享同一个缓冲区,通过引用计数来决定何时释放此缓冲区。但较新版本的 C++ 规范有效地杜绝了这种表示法,所有现代 C++ 库使用的都是这里展示的方法。

在这些情况下,人们普遍认为,虽然其他代码也可以创建指向所拥有内存的临时指针,但在拥有者决定销毁拥有的对象之前,其他代码有责任确保其指针已消失。也就是说,你可以创建一个指向 std::string 的缓冲区中的字符的指针,但是当字符串被销毁时,你也必须让你的指针失效,并且要确保不再使用它。拥有者决定被拥有者的生命周期,其他所有人都必须尊重其决定。

这里使用了 std::string 作为 C++ 中所有权的示例:它只是标准库通常遵循的规约,尽管 C++ 鼓励人们都遵循类似的做法,但说到底,如何设计自己的类型还是要由你自己决定。

然而,在 Rust 中,所有权这个概念内置于语言本身,并通过编译期检查强制执行。每个值都有决定其生命周期的唯一拥有者。当拥有者被释放时,它拥有的值也会同时被释放,在 Rust 术语中,释放的行为被称为丢弃(drop)。这些规则便于通过检查代码确定任意值的生命周期,也提供了系统级语言本应支持的对生命周期的控制。

变量拥有自己的值,当控制流离开声明变量的块时,变量就会被丢弃,因此它的值也会一起被丢弃。例如:

代码语言:javascript
复制
fn print_padovan() {
    let mut padovan = vec![1,1,1];  // 在此分配
    for i in 3..10 {
        let next = padovan[i-3] + padovan[i-2];
        padovan.push(next);
    }
    println!("P(1..10) = {:?}", padovan);
}                                   // 在此丢弃

变量 padovan 的类型为 Vec<i32>,即一个 32 位整数向量。在内存中,padovan 的最终值如图 4-2 所示。

图 4-2:栈上的 Vec<i32>,指向其在堆中的缓冲区

这和之前展示过的 C++ std::string 非常相似,不过缓冲区中的元素都是 32 位整数,而不是字符。请注意,保存 padovan 指针、容量和长度的字都直接位于 print_padovan 函数的栈帧中,只有向量的缓冲区才分配在堆上。

和之前的字符串 s 一样,此向量拥有保存其元素的缓冲区。当变量 padovan 在函数末尾超出作用域时,程序将会丢弃此向量。因为向量拥有自己的缓冲区,所以此缓冲区也会一起被丢弃。

Rust 的 Box 类型是所有权的另一个例子。Box<T> 是指向存储在堆上的 T 类型值的指针。可以调用 Box::new(v) 分配一些堆空间,将值 v 移入其中,并返回一个指向该堆空间的 Box。因为 Box 拥有它所指向的空间,所以当丢弃 Box 时,也会释放此空间。

例如,可以像下面这样在堆中分配一个元组:

代码语言:javascript
复制
{
    let point = Box::new((0.625, 0.5));  // 在此分配了point
    let label = format!("{:?}", point);  // 在此分配了label
    assert_eq!(label, "(0.625, 0.5)");
}                                        // 在此全都丢弃了

当程序调用 Box::new 时,它会在堆上为由两个 f64 值构成的元组分配空间,然后将其参数 (0.625, 0.5) 移进去,并返回指向该空间的指针。当控制流抵达对 assert_eq! 的调用时,栈帧如图 4-3 所示。

图 4-3:两个局部变量,它们各自在堆中拥有内存

栈帧本身包含变量 pointlabel,其中每个变量都指向其拥有的堆中内存。当丢弃它们时,它们拥有的堆中内存也会一起被释放。

就像变量拥有自己的值一样,结构体拥有自己的字段,元组、数组和向量则拥有自己的元素。

代码语言:javascript
复制
struct Person { name: String, birth: i32 }

let mut composers = Vec::new();
composers.push(Person { name: "Palestrina".to_string(),
                        birth: 1525 });
composers.push(Person { name: "Dowland".to_string(),
                        birth: 1563 });
composers.push(Person { name: "Lully".to_string(),
                        birth: 1632 });
for composer in &composers {
    println!("{}, born {}", composer.name, composer.birth);
}

在这里,composers 是一个 Vec<Person>,即由结构体组成的向量,每个结构体都包含一个字符串和数值。在内存中,composers 的最终值如图 4-4 所示。

图 4-4:更复杂的所有权树

这里有很多所有权关系,但每个都一目了然:composers 拥有一个向量,向量拥有自己的元素,每个元素都是一个 Person 结构体,每个结构体都拥有自己的字段,并且字符串字段拥有自己的文本。当控制流离开声明 composers 的作用域时,程序会丢弃自己的值并将整棵所有权树一起丢弃。如果还存在其他类型的集合(可能是 HashMap 或 BTreeSet),那么处理的方式也是一样的。

现在,回过头来思考一下刚刚介绍的这些所有权关系的重要性。每个值都有一个唯一的拥有者,因此很容易决定何时丢弃它。但是每个值可能会拥有许多其他值,比如向量 composers 会拥有自己的所有元素。这些值还可能拥有其他值:composers 的每个元素都各自拥有一个字符串,该字符串又拥有自己的文本。

由此可见,拥有者及其拥有的那些值形成了一棵:值的拥有者是值的父节点,值拥有的值是值的子节点。每棵树的总根都是一个变量,当该变量超出作用域时,整棵树都将随之消失。可以在 composers 的图中看到这样的所有权树:它既不是“搜索树”那种数据结构意义上的“树”,也不是由 DOM 元素构成的 HTML 文档。相反,我们有一棵由混合类型构建的树,Rust 的单一拥有者规则将禁止任何可能让它们排列得比树结构更复杂的可能性。Rust 程序中的每一个值都是某棵树的成员,树根是某个变量。

Rust 程序通常不需要像 C 程序和 C++ 程序那样显式地使用 freedelete 来丢弃值。在 Rust 中丢弃一个值的方式就是从所有权树中移除它:或者离开变量的作用域,或者从向量中删除一个元素,或者执行其他类似的操作。这样一来,Rust 就会确保正确地丢弃该值及其拥有的一切。

从某种意义上说,Rust 确实不如其他语言强大:其他所有实用的编程语言都允许你构建出任意复杂的对象图,这些对象可以用你认为合适的方式相互引用。但正是因为 Rust 不那么强大,所以编辑器对你的程序所进行的分析才能更强大。Rust 的安全保证之所以可行,是因为在你的代码中可能出现的那些关系都更可控。这是之前提过的 Rust 的“激进赌注”的一部分:实际上,Rust 声称,解决问题的方式通常是灵活多样的,因此总是会有一些完美的解决方案能同时满足它所施加的限制。

迄今为止,我们已经解释过的这些所有权概念仍然过于严格,还处理不了某些场景。Rust 从几个方面扩展了这种简单的思想。

  • 可以将值从一个拥有者转移给另一个拥有者。这允许你构建、重新排列和拆除树形结构。
  • 像整数、浮点数和字符这样的非常简单的类型,不受所有权规则的约束。这些称为 Copy 类型。
  • 标准库提供了引用计数指针类型 RcArc,它们允许值在某些限制下有多个拥有者。
  • 可以对值进行“借用”(borrow),以获得值的引用。这种引用是非拥有型指针,有着受限的生命周期。

这些策略中的每一个策略都为所有权模型带来了灵活性,同时仍然坚持着 Rust 的那些承诺。

笔记 Rust 通过一些限制的方式保证安全性,同时提供了对应的灵活性 Rust中也提到了生命周期,这里想到了前端Vue框架中的生命周期,一个对应变量,一个对应组件

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-13,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 草帽Lufei 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 4.1 所有权
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com