前言
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感兴趣的胖友也欢迎与我交流探讨。
个人邮箱:mozheng.sh@alibaba-inc.com
个人微信:longmaost