首页 > 系统相关 >深入浅出Rust所有权:手把手从零设计Rust所有权体系,掌握Rust内存管理思想的精髓

深入浅出Rust所有权:手把手从零设计Rust所有权体系,掌握Rust内存管理思想的精髓

时间:2024-06-09 16:04:26浏览次数:14  
标签:手把手 规则 引用 借用 所有权 Rust 内存

撰写编程语言发展历史过程中,对Rust的所有权机制的设计进行了深入的探讨,摘取其中的一段内容,邀请大家点评。

Rust的所有权机制,看似复杂且与现有编程语言不同,使用起来思路也许难以适应。是学习Rust的难点。但如果我们换个思路,假设我们是Rust的设计者,逐步深入Rust的内心世界,也许更容易掌握Rust所有权的思想和用法。

历史难题

在软件工业界,内存管理一直是一个棘手的问题。主要有两大问题尚未解决:

  1. 内存泄露
  2. 垂悬指针

编程语言探索了多种内存管理方式,但都未能彻底解决这两个问题。例如,自动垃圾回收能够很好地解决垂悬指针问题,但它不能完全避免内存泄露,更不用说手动内存管理或引用计数了。

是否有可能设计一种机制,能够彻底解决内存泄露和垂悬指针的问题呢?

开始设计

让我们首先考虑,导致内存泄露和垂悬指针的根本原因是什么?

  1. 内存泄露的原因:循环引用、使用后未释放
  2. 垂悬指针的产生:某处释放了指针,而另一处还尝试读写

如果我们设计一种机制,禁止开发者编写出含有“循环引用”和“同时存在多个读写引用”的代码,并提供“使用后自动释放”的机制,是不是就能彻底解决这个问题了?

在现有编译器的能力下,提供使用后自动释放的机制并不困难。我们可以规定一种机制,这种机制可以追踪和管理每个值的内存,并确保当一个值不再被需要时,它能够及时被销毁。

接下来,我们重点关注“循环引用”和“同时存在多个读写引用”。

“循环引用”的本质是所有权不明确。为了避免这种情况,我们需要经常地将所有权转移,于是我们决定这样规定:

  1. 所有权可以转移,并且需要经常转移。
  2. 当一个值从一个变量赋值给另一个变量时,所有权就从这个变量转移到了另一个变量。
  3. 新变量拥有了这个值的所有权,旧变量失去了这个值的所有权,旧变量无效了。
  4. 每个值都有一个变量作为其所有者,且在任意时刻只能有一个所有者。

通过所有权的概念,结合及时销毁的原则,我们需要引入生命周期(lifetimes)的概念,对这些规定进行准确的定义。

生命周期(lifetimes):标识了从创建(或绑定)到销毁(或解绑)的整个时期,是值、变量或引用在内存中存在的时间段。

结合生命周期(lifetimes)概念,我们可以将追踪和管理内存的规则优化为:

  • 值的生命周期(lifetimes)在其所有权拥有者离开其作用域时结束,值及其占用的内存都将被释放。

但是,在现实世界的编程中,我们通常需要多个变量来引用同一个值。在现有的概念框架下,就需要共享所有权,但共享所有权打破了我们“任意时刻只能有一个所有者”的规则,重新导致循环引用问题出现。为了遵循前面我们制定的已有规则,同时允许其他变量访问这个值,我们需要对所有权概念进行细化,增加一些限制,引入了借用(borrowing)概念。

借用不拥有值的所有权但是可以访问值

借用不会增加值的生命周期,因此即使在循环引用的情况下,生命周期也不会被无限延长,从而避免了内存泄露。

到目前为止,这套机制已经能够很好地解决内存泄露问题。但还有一个“垂悬指针”问题待解。垂悬指针的根本问题在于存在多个可读写引用,当一处释放了内存,而另一处还在尝试访问时,就会出现问题。

我们需要一套规则来避免同时存在多个可读写引用。如果一个值有多个可读引用,这是安全的,但一旦出现一个可写引用,无论是读还是写,都是危险的。因此,我们需要进一步细化借用,并制定以下规则:

  1. 借用分为可变借用和不可变借用。可变借用产生的是可变引用的变量,不可变借用产生的是不可变引用的值。
  2. 如果一个值存在一个可变引用(&mut),那么它必须是唯一的。不能同时存在其他的可变引用或不可变引用。
  3. 如果一个值存在一个或多个不可变引用(&),则不能同时存在可变引用。

2和3的规则,和读写锁的规则很像。可见编程世界里的很多概念有其内在的相通之处。

通过新引入的规则,确保了在任何给定的时间,要么只有一个可变引用对值进行修改,要么有多个不可变引用进行读取,但不允许同时进行读写。这样一来,就能避免数据竞争,防止垂悬指针的产生。通过这套规则,我们有效地解决了“垂悬指针”的问题。

至此,我们再次梳理并总结一下我们之前制定的规则:

  1. 所有权决定生命周期。
  2. 每个值只能有一个所有权拥有者。
  3. 所有权可以转移。
  4. 借用允许创建指向值的引用。
  5. 借用分为可变引用和不可变引用。
  6. 可变引用(&mut)必须是唯一的。不能同时有其他的可变引用或不可变引用存在。
  7. 不可变引用(&)可以存在多个,但不能和可变引用(&mut)共存。
  8. 编译器确保所有借用值的作用域不超过其值的生命周期。
  9. 编译器确保每一个值的所有权者离开作用域时,销毁所拥有的对象。

举个栗子

为了加深理解,我们用一个现实生活中的场景来类比思考:
假设我们正在组织一场音乐会,音乐会上的乐器代表程序中的数据。在音乐会上,每个乐器(数据)在任何时刻只能由一位乐手(变量)负责演奏(拥有)。这位乐手对乐器有完全的控制权,可以决定何时开始演奏、何时停止,以及如何演奏。这个约定是所有权概念的体现:一段数据在任何时刻只有一个所有者。
现在,如果这位乐手需要休息,需要将乐器留在了原地,其他乐手可以来尝试演奏这个乐器,但必须遵守一些规则:

  • 如果一位乐手只是想试听乐器的声音,不打算改变任何设置(即只读访问),那么可以有多位乐手同时这样做。这就像不可变借用(&T),允许以只读方式借用数据。

  • 如果另一位乐手想要调整乐器(即写入访问),比如调整琴弦的松紧,以便以不同的风格演奏,他们必须等到没有其他人在试听乐器时才能进行。这是可变借用(&mut T),它确保在可变借用期间没有其他的借用。

  • 一旦调整乐手完成他们的调整并将乐器交回,原来的乐手可以继续演奏。这是当可变引用结束时,原始数据的所有权会返回给所有者。

  • 如果原来的乐手决定不再演奏,他们可以将乐器传递给另一个乐手,使得新的乐手成为乐器的新所有者。这可以看作是所有权转移。一旦所有权转移发生,原所有者将无法再访问那段数据。

  • 为确保音乐会上的每件乐器在整个活动结束时都能得到妥善管理,组织者(即编译器)会跟踪每个乐器的负责人,并确保在音乐会结束时,所有乐器都已被妥善回收。这样就避免了“忘记的乐器”(内存泄漏)或演奏中的混乱(垂悬指针)。

在这个类比中,组织者确保乐器的使用遵循一套明确的规则,预防混乱和潜在的冲突。类似地,在我们设计的内存安全的规则中,编译器保证内存访问遵守所有权、借用和生命周期的规则,防止数据竞争和无效访问,从而确保程序的安全性。

完善体系


实际上,Rust还需要一些其他附加的规则,让这套规则在实际的编程场景中更加好用和更安全。这些规则包括:

  1.  一般语言的赋值运算符(=)、参数传参默认的语义是复制,而Rust默认的赋值语义是所有权转移(移动语义)(对于实现了Copy特性的类型(例如基本数值类型),Rust默认使用复制语义),这个区别是Rust初学者需要首先适应的思维差异。
  2. 一些特殊情况下,编译器无法准确判断对象生命周期,因此引入了生命周期标注来让编译器知道引用的有效范围。
  3. 变量默认是不可变的,这样更容易创建不可变引用,避免不必要的可变引用导致编程的限制增多。因此可变引用必须通过mut显式地声明可变。


到这里,我们设计给规则,基本涵盖了Rust的所有权体系中核心的理念和规则了。

Rust通过所有权体系的这些规则和限制,成功地创建了一种能够在编译时强制执行内存安全的编程语言,并且无需依赖运行时的垃圾回收器(影响运行时性能)。这使得Rust非常适合系统编程,为构建高性能应用程序提供了强大的工具。

在我们设计的过程中,我们增加了许多核心规则都限制了变量的访问场景,我们可以隐约感受到这套规则使用起来确实会和已有的编程语言有很大的差别,甚至这些限制可能会给开发者造成一定的不便。但是这些核心规则虽然增加了学习的难度,但一旦掌握,它们将使程序员能够编写出更安全、更高效的代码。

随着Rust生态系统的不断成熟,以及社区提供的丰富资源和工具,Rust越来越受欢迎,并在各种领域得到应用。

但是,并发编程(多线程编程)中存在的内存访问冲突(也称为内存竞争)的问题,仍然是编程界另一大难题,读到这里的读者,相信对技术非常有想法,也许下一个解开这个难题的人就是你!

标签:手把手,规则,引用,借用,所有权,Rust,内存
From: https://blog.csdn.net/hebhljdx/article/details/139439157

相关文章

  • 《手把手教你》系列练习篇之15-python+ selenium自动化测试 -番外篇 - 最后一波啊!!!(详细
    1.简介 本来上一篇就是练习篇的最后一篇文章了,但是有的小伙伴私下反映说是做了那么多练习,没有一个比较综合的demo练练手。因此宏哥在这里又补存了一些常见的知识点进行练习,在文章最后也通过实例给小伙伴们或者童鞋们进行了一个登录模块的自动化测试的实例,其他的你可以照......
  • 《手把手教你》系列练习篇之14-python+ selenium自动化测试 -压台篇(详细教程)
    1.简介 本文是练习篇的最后一篇文章,虽然练习篇的文章到此就要和大家说拜拜了,但是我们的学习之路才刚刚开始。不要停下你的脚步,大步朝前走吧!比你优秀的人还在走着,我们有什么理由停下自己的脚步了,生命不止,学习亦是如此。好了,宏哥的毒鸡汤好喝吧,喝够了就开始学习吧。......
  • 跨语言系统中的功能通信:Rust、Java、Go和C++的最佳实践
    在现代软件开发中,使用多种编程语言构建复杂系统已成为一种常见的做法。每种编程语言都有其独特的优势和适用场景,这使得在同一个系统中使用多种语言变得合理且高效。然而,这也带来了一个重要的挑战:如何在这些不同语言之间实现高效、可靠的功能通信。本文将探讨Rust、Java、Go和C+......
  • RustDesk 搭建
    Web、API部署教程:https://www.52pojie.cn/thread-1708319-1-1.htmlRustDesk服务端下载:https://github.com/rustdesk/rustdesk-server/releasesRustDesk客户端下载:https://github.com/rustdesk/rustdesk/releases/tag/1.2.3-2RustDesk官方部署教程:https://rustdesk.com/docs......
  • 手把手制作Vue3+Flask全栈项目 全栈开发之路实战篇 问卷网站(五)数据处理
    全栈开发一条龙——前端篇第一篇:框架确定、ide设置与项目创建第二篇:介绍项目文件意义、组件结构与导入以及setup的引入。第三篇:setup语法,设置响应式数据。第四篇:数据绑定、计算属性和watch监视第五篇:组件间通信及知识补充第六篇:生命周期和自定义hooks第七篇:路由......
  • RUST安装和配置过程
    RUST安装和配置过程在Linux系统下,使用如下命令执行安装sudosh-c"curl--proto'=https'--tlsv1.2-sSfhttps://sh.rustup.rs|sh"可能会有报错如下检查/tmp权限确保/tmp目录具有正确的权限,允许所有用户写入。可以使用以下命令检查/tmp目录的权限:ls-ld/tmp......
  • 【Rust】——面向对象设计模式的实现
     ......
  • Rust基础学习-Rust宏
    Rust中的宏是生成另一段代码的一段代码。可以根据输入生成代码,简化重复模式,使得代码更加简洁。比如我们一直在用的println!,vec!,panic!都是宏。创建宏可以使用macro_rules!创建一个宏:macro_rules!macro_name{(...)=>{...}}这里的()=>{}是宏规则条目,我......
  • rust服务控制之使用rs-svc(rust svc)实现优雅退出
    使用rs-svc实现rust程序的优雅退出.引用依赖在Cargo.toml中添加rs-svc和anyhow库:[package]name="svctest"version="0.1.0"edition="2021"#Seemorekeysandtheirdefinitionsathttps://doc.rust-lang.org/cargo/reference/manifest.html[depen......
  • AI绘画软件的实际应用场景有哪些?手把手教会你
    在数字化时代,AI绘画软件正迅速成为艺术创作和设计领域的新宠。它不仅能够提高工作效率,还能帮助创作者突破传统的艺术创作边界。本文将重点介绍AI绘画软件—STARTAI在摄影后期和电商场景中的实际应用,以及具体的操作教程。一、AI绘画软件在摄影后期的应用摄影后期处理是摄影师......