前言
rust 是 mozilla 公司出品的一门系统级编程语言,其主要特点是性能良好 ( 号称不输 c++),运行安全以及文档与工具链完善。 rust 在前端方面也有很多应用,它是目前 webassembly 生态支持度最高的编程语言,这个可能也跟历史原因有关, webassembly 的前身 asm.js 也是 mozilla 公司出品,有兴趣的同学可以自行查找相关资料。此外,大名鼎鼎的 deno 其底层实现也是基于 rust 编写。本文主要介绍令很多初学者感到懵圈,对编写 rust 代码产生人生怀疑的杀手 -ownership
Ownership 是什么
Ownership,中文翻译是所有权,它是 rust 程序的一套内存管理策略。对于编程语言来说,内存管理是一个永恒的话题,有的编程语言自带垃圾回收( GC), GC 会负责追踪和清除不再使用的内存变量,比如我们常写的 js。对于没有 GC 的编程语言来说,就需要开发者在编码中自行确定哪些变量不再使用并手动清除它,这对于开发者来说显然是一个比较大的负担,如果操作不当极易导致内存泄露或者空指针之类的异常,典型的比如 c/c++。在内存管理策略上 rust 属于另辟蹊径,它会在编译阶段按照 ownership 规则检查你程序中的内存隐患,从而保证程序在运行时的安全稳定。 ownership 规则可以简约的概括为以下三点:
- 在 rust 程序中每一个被赋值的变量是该值的所有者
- 对于一个值来说,同一时间只能存在一个所有者
- 当所有者所属作用域结束时,所有者的值也会被销毁
这三点先不做深入介绍,可以先在脑子里留个印象,后续会讲到。目前我们需要知道的是为什么 rust 会有 ownership 机制,它具体解决什么问题。
Ownership 解决什么问题
ownership 主要解决的是堆上数据内存的管理问题。相信有过编程经验的同学都知道,基本上不论是强类型还是弱类型编程语言都存在简单数据类型,诸如 string, int, float,boolean 和复杂数据类型,诸如 struct, object 等等。不同的数据类型占用的内存区域是不同的。对于简单数据类型来说,在强类型语言中,它们在声明的时候可使用的内存大小是固定已知的,这部分数据存放在内存中的栈( stack)区。对于复杂数据类型来说,由于其数据值会动态变化,因此其所占内存大小无法在编译阶段确定,其在内存中存储时使用的是堆( heap)区。一个典型场景是在开发命令行应用时,你需要将用户的输入赋值给一个变量,因为用户输入是不可预知的,所以也只能在运行时才能确定该变量需要的内存空间大小。
不论是简单类型还是复杂类型,当其所在作用域结束时 rust 内部会自动调用 drop 方法清除其所占内存,但问题是如果有多个变量同时指向同一块内存区域时如果这些变量都被销毁,会导致同一块内存区域被释放多次,这同样会导致内存崩溃,带来安全隐患。而 ownership 就是 rust 对于这个问题的解法,那么它是如何在代码层面来体现的呢?且看如下分解
Ownership 的代码体现
首先来看一组代码与执行结果演示:
- 简单类型:
执行结果: - 复杂类型:
执行结果:
上面两组代码的执行逻辑基本一致,但是所得结果却完全不同。在示例一中, x 变量的值绑定到 y 后,打印 x 和 y 的值可以正常显示想要的结果。但在示例二中, s1 值绑定到 s2 后,当打印 s1 和 s2 值时 rust 却在编译阶段就直接报错了,这是为什么呢?。简单类型与复杂类型在通过变量赋值时的内部逻辑难道有所差异?没错,确实是存在巨大差异,我会用几张示意图来进行演示说明。
在示例一种因为变量 x 是简单类型,因此其值是存放在栈上的,当赋值给变量 y 时执行的其实是值拷贝的过程,其结果是会在栈内存上新建一个空间存放 y 的值,示意图如下:
因为 x 和 y 分别指向不同的内存区域,在 main 函数结束释放内存时也就不存在内存所有权冲突问题,因此在 main 函数末尾打印 x 和 y 变量的值可以正常展示。
在示例二中 String 是复杂类型(此处不要带入 js 的概念,在 rust 中 String 用于存储动态变化的字符串,属于复杂类型),变量 s1 的值复制到 s2 时执行的其实是指针的复制,示意图如下:
从 s1 到 s2 ,其指针指向的都是堆区的同一个内存空间,在这种情况下当 s1 和 s2 所在作用域结束需要销毁变量释放内存时如果不加处理就会发生同一块内存区域被释放多次的问题,为了避免这个问题, rust 采用了 ownership 机制,回顾前面我们介绍 ownership 时提到的三点概括,对于一个值来说,同一时间只能存在一个所有者。当 s1 赋值到 s2 时因为这两个所有者指向的是同一个内存空间,因此在赋值完成后 s1 会被销毁, s2 接过该内存空间的所有权,这样导致的结果就是我们在示例二程序的执行结果中看到的那样,变量 s1 不能继续访问。示意图如下:
除了显式的变量赋值以外,常见的函数参数传递也是大型 ownership 交接现场。示例代码如下所示:
顺道再多扯一句,这种指针的复制与 js 中复杂类型的引用传递比较类似,但 js 中引用传递时并不会像 rust 这样销毁 s1, 而是 s1,s2 同时存在,且一旦 s2 的值有所变更, s1 的值也会更新,这也给 js 开发者在运行时追踪数据变更相关的 bug 增加了困难,这也是为什么 js 会有各种各样的 immutable 库。
结语
相信到这里大家已经对 rust 的 ownership 机制有了比较直观的认识。 ownership 的核心点在于同一时间同一内存只能存在一个所有者,在变量作用域结束时 rust 会释放该变量占有的内存空间。 ownership 对于 rust 的代码编写方式也产生了很大的影响,刚开始可能会很不适应,但随着更多的编写 rust 代码,最终也会变得得心应手。如果有对 rust 感兴趣的胖友也欢迎与我交流探讨。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章