我们再使用一个例子,来继续说明为什么
Rust
的“mutation+alias”规则是有必要
的。
我们这次通过制造一个悬空指针来解释。以下为一段合理的
C++
代码,它创建
了一个动态数组,然后使用了一个指针,指向了动态数组的内部元素,然后我们向
动态数组内添加内容,然后发现原先的指针“悬空”了,它指向了一个非法的地址:
// 以下仅仅为了示例而已,不代表推荐的C++编码风格
#include <vector>
#include <iostream>
using namespace std;
int main(){
vector<int>v(100,5);// 指针指向内部第一个元素
int * p0 =&v[0];
cout <<*p0 << endl;// 为了确保v发生扩容,多插入一些数据for(int i =0; i<100; i++){
v.push_back(10);}// 打印p0的内容
cout <<*p0 << endl;return0;}
编译通过,执行结果为:
5-72140872
熟悉STL的朋友肯定知道这里究竟发生了什么。动态数组是自行管理内存空间
的,在向动态数组内部添加元素的时候,如果超过了当前的最大容量,这个动态数
组会申请一块更大的连续内存空间,将原来的元素移动过去,释放掉之前的内存空
间,然后继续往后面添加元素。
我们的指针一开始是指向动态数组的第一个元素的,但是当往动态数组内部添
加多个元素之后,之前的那块内存已经不够用了,动态数组在这个过程中已经将原
来的内存空间释放,并申请了新的内存空间。于是,原本应该指向数组第一个元素
的指针从一个合法的指针变成了指向已回收内存区域的悬空指针,它现在指向的数
据是与原来的意图不同的。而这种情况正是属于Rust希望解决的“内存安全”问题。
我们来看看用Rust写会发生什么。同样,使用动态数组类型,使用一个指针指
向它的第一个元素,然后在原来的动态数组中插入数据:
fnmain(){letmut arr :Vec<i32>=vec![1,2,3,4,5];let p :&i32=&arr[0];for i in1..100{
arr.push(i);}}
编译不通过,错误信息为:
error: cannot borrow `arr` as mutable because it is also borrowed as immutable
我们可以看到,“mutation+alias”规则再次起了作用。在存在一个不可变引用的
情况下,我们不能修改原来变量的值。写Rust代码的时候,经常会有这样的感觉:
Rust
编译器极其严格,甚至到了“不近人情”的地步。但是大部分时候却又发现,它
指出来的问题的确是对我们编程有益的。对它使用越熟练,越觉得它是一个好帮
手。
总结
Rust
在内存安全方面的设计方案的核心思想是“共享不可变,可变不共享”。
在可变性控制方面,如果说,C语言和函数式编程语言分属一个天平的两端,
那么Rust就处于这个天平的中央。C语言的思想是:尽量不对程序员做限制,尽量
接近机器底层,类型安全、可变性、共享性都由程序员自由掌控,语言本身不提供
太多的限制和规定。安全与否,也完全取决于程序员。而函数式编程的思想是:尽
量使用不可变绑定,在可变性上有严格限制,在共享性方面没有限制。
函数式编程特别强调无副作用的函数以及不可变类型,以此来达到提高安全性的目的。
而Rust则是选择了折中的方案,既允许可变性,也允许共享性,只要这两者不
是同时出现即可。“共享不可变,可变不共享”,是Rust保证内存安全和线程安全
的“法宝”。
而我们可以看到,
Rust
的这个设计并不是首鼠两端、和稀泥式的中庸之
道,而是经过了仔细的观察总结、严谨的设计之后的产物。
其一,相比函数式设计方式,
Rust
并没有本质上牺牲安全性。函数式编程强调
的“不可变”特性,极大地提升了安全性的同时,也极大地提高了学习门槛。而Rust
在“不可变”要求上的理性妥协,实现了在不损失安全性的同时,一定程度上也降低
了学习成本。从C/C++背景转为使用Rust无需做太大的思维转变。相比函数式的设
计方式,
Rust
的入门门槛更低。虽然对于习惯了无拘无束自由挥洒的C/C++编程语
言的朋友来说,还是有诸多不习惯,但毕竟比Haskell要容易得多。
其二,
Rust
针对传统C/C++做了大幅改进,设计了一系列静态检查规则,来防
止一些潜在的bug。“共享不可变,可变不共享”就是其中一项重要的规则。在传统
的C/C++中,所有的指针都是同一个类型。从功能性来说,这样设计是非常强大
的,但它缺少的恰恰是一定程度的取舍,以提高安全性。相对来说,Rust对程序员
的限制更多,有所为、有所不为。鼓励用户使用的功能应当越容易越优雅越好;避
免用户滥用的功能应当越困难越复杂越好。二者不可偏废。
其三,
Rust
的这套内存安全体系,不需要依赖GC。虽然现在GC的性能越来越
好,但是没有GC在某些场景下依然是很重要的。没有GC、编译型语言的特点,是
Rust执行性能的潜力保证。这就是为什么Rust设计组有底气说Rust的运行性能与C语
言处于同一个档次的原因。当然,目前的Rust还很年轻,许多优化还没有实现,但
这不要紧,单从技术层面上看,还有许多优化在可行性上是没问题的,唯一需要的
是时间和工作量。另外,没有GC就可以使得它只依赖一个非常轻量级的runtime。
理论上来说,它可以用于许多嵌入式平台,甚至可以在无操作系统的裸机上执行,
使用Rust编写操作系统也是完全可行的。这就使得Rust拥有与C/C++相似的系统级
编程特性,大幅扩展了Rust的应用场景。
其四,
Rust
的核心思想“共享不可变,可变不共享”,具有极好的一致性和扩展
性。它不仅可以解决内存安全的问题,还是解决线程安全的基础。在后文中我们会
看到,所谓的线程安全,实质上就是内存安全在多线程情况下的自然延伸。反过
来,我们也可以把Rust的内存安全解决方案视为传统的线程安全机制Read Write
Locker的编译阶段执行的版本。大家应该都能联想到,在多线程环境下,数据竞争
问题是怎么出现的。如果多个线程对同一个共享变量都是只读的,它是安全的;如
果有一个线程对共享变量写操作,那它就必须是独占的,不可有其他线程继续读
写,否则就会出现数据竞争。在第四部分中我们还会发现,Rust里面的许多线程安
全的类型,与一些非线程安全的类型,具有非常有趣的对称性。
由此我们可以看出,
Rust
的这套设计方案的确是有创新性的。
它走出了一条前无古人的道路。
Rust
在其他方面的功能,都不能被称作原创设计,都是从其他编程
语言中学过来的。唯独安全性方面的设计是独一无二的。只要我们保证了“共享不
可变,可变不共享”,我们就可以保证内存安全。那么它这套设计方案,究竟能不
能被大众所接受呢?我们拭目以待。
另外,这个规定是否是过于严苛了呢?会不会大幅削弱代码的表达能力?后面
我们还需要进一步分析。
版权归原作者 断水流大撕兄 所有, 如有侵权,请联系我们删除。