# 揭秘前端眼中的Rust
51CTO2022-09-18
来源 | 腾讯云开发者
作者 | 于玉龙
本文主要对rust相关内容进行解读分析,希望本文能对此方面感兴趣的开发者们提供一些经验和帮助。
关于Rust
rust是一门强类型的、编译型的、内存安全的编程语言。最早版本的Rust原本是Mozilla基金会的一名叫Graydon Hoare的员工的私人项目,2009年开始,Mozilla开始赞助者们项目的发展,并于2010年,Rust实现了自举——使用Rust构建了Rust的编译器。
Mozilla将Rust应用到构建新一代浏览器排版引擎Servo当中——Servo的CSS引擎在2017年开始,集成到了FireFox当中去。
Rust原本作为一种内存安全的语言,其初衷是代替C++或者C,来构建大型的底层项目,如操作系统、浏览器等,但是因为Mozilla的这一层关系,前端业界也注意到了这门语言,并将它应用在了其他领域,其生态也慢慢繁荣起来。
内存安全——Rust的一大杀手锏
众所周知,当下主流的编程语言当中一般分为两类,一类是自动GC的,如Golang、Java、JavaScript等,另一类则是C++和C,用户需要手动管理内存。
大部分语言的内存模型都是大同小异的。
当代码被执行时,一个个变量所对应的值,就被依次入栈,当代码执行完某一个作用域时,变量对应的值也就跟着出栈,栈作为一个先进后出的结构非常符合编程语言的作用域——最外层的作用域先声明、后结束。但是栈无法在中间插入值,因此栈当中只能存储一旦声明、占用空间就不会改变的值,比如int、char,或者是固定长度的数组,而其他值,比如可变长度的数组vector,可变长度的字符串String,是无法被塞进栈当中的。
当编程语言需要一个预先不知道多大的空间时,就会向操作系统申请,操作系统开辟一块空间,并将这一块空间的内存地址——指针返回给程序,于是编程语言就成功将这些数据存到了堆中,并将指针存到栈当中去——因为指针的大小是固定的,32位程序的指针一定是32bit,64位程序的指针也肯定是64bit。
栈中的数据是不需要做内存管理的,随着代码执行,一个变量很容易被判断还有没有用——只要这个变量的作用域结束,那么再也无法读取到这个变量的值,那么这个变量肯定没用了。只需要随着作用域的声明与结束,不断的入栈和出栈就足以管理栈的内存了,不需要程序员操心。
但是堆当中的数据就不行了,因为程序拿到的只是一个内存指针,实际的内存块不在栈当中,无法随着栈自动销毁。程序也不能在栈当中的内存指针变量销毁时,就将指针对应的空间自动清理——因为可能有多个变量保存的指针都指向了同一个内存块,此时清理这个内存块,会导致意料之外的情况。
基于此,有的程序自带一套非常复杂的GC算法,比如通过引用计数,统计一个内存区块的指针到底保存在多少个变量当中,当引用计数归0时,就代表所有的指向此处的指针都被销毁了,此处内存块就可以被清理。而有的程序则需要手动管理内存空间,任何堆当中开辟的空间,都必须手动清理。
这两种办法各有优劣,前者导致程序必须带一个runtime,runtime当中存放GC算法,导致程序体积变大,而后者,则变得内存不安全,或者说,由于内存管理的责任到了程序员头上,程序员的水平极大程度上影响了代码安全性,忘记回收会导致程序占用的内存越来越大,回收错误会导致删掉不应该删的数据,除此以外还有通过指针修改数据的时候溢出到其他区块导致修改了不应修改的数据等等。
而Rust则采取了一种全新的内存管理方式。这个方式可以简单概括为:程序员和编译器达成某一种约定,程序员必须按照这个约定来写代码,而当程序员按照这个约定来写代码时,那么一个内存区块是否还在被使用,就变得非常清晰,清晰到不需要程序跑起来,就可以在编译阶段知道,那么编译器就可以将内存回收的代码,插入到代码的特定位置,来实现内存回收。换句话说,Rust本质上是通过限制引用的使用,将那些【不好判断某块地址是否还在使用】的情况给规避了,剩余的情况,都是很好判断的情况,简单到不需要专业的程序员,只需要一个编译器,就能很好的判断了。
这样的一大好处是:
不需要GC算法和runtime,本质上还是手动回收,只不过编译器把手动回收的代码插入进去了,程序员不需要自己写而已。
只要编译可以通过,那么就一定是内存安全的。
实现原理
rust的内存安全机制可以说是独创的,它有一套非常简单、便于理解的机制,叫做所有权系统,这里面会涉及到两个核心概念,所有权和借用。
所有权
任何值,包括指针,都要绑定到一个变量,那么,我们就称这个变量拥有这个值的所有权,比如以下代码,变量str就拥有“hello”的所有权。
当str所在的作用域结束时,str的值就会被清理,str也不再有效。这个和几乎所有主流语言都是一致的,没有什么问题。也很好理解。
但是注意一下,Rust本身区分了可变长度的字符串和不可变长度的字符串,上文是一个不可变长度的字符串,因为其长度不可变,可以保存在栈当中,于是下面这一段代码可以正确执行,就像其他几乎所有主流语言一样:
但如果我们引入一个保存在堆里、长度可变的字符串,我们再来看看同样的代码:
此时,我们会惊讶地发现,代码报错了。为什么呢?
原因在于,第一段代码当中,str这个变量的值,保存在栈里,str这个变量所拥有的,是hello world这一串字符串本身。所以如果令str2=str,那么相当于又创建了一个str2变量,它也拥有这么一串一模一样的字符串,这里发生的是“内存拷贝”。两个变量各自拥有hello world这一个值的所有权,只不过两者的hello world不是同一个hello world。
而第二段代码当中,我们拿到的str,本质上只是一个指向到某一个内存区块的地址,而这个地址,当我们另str2=str的时候,实际上是将这一个地址的值赋值给str2,如果是在其他语言当中,这么写极大概率是没问题的,但是str和str2会指向同一个内存地址,修改str的时候,str2也变了。但是rust当中,同一个值只能被绑定到一个同一个变量,或者说,某一个变量对这一个值有所有权,就像一个东西同一时间只能属于同一个人一样!当令str2=str的时候str保存的地址值,就不再属于str了
登录后可查看完整内容,参与讨论!
立即登录