电子网站建设价格,重庆市,汕尾旅游攻略app跳转网站,wordpress注册页模板Rust语言的核心特点是#xff1a;在没有放弃对内存的直接控制力的情况下#xff0c;实现了内存安全。
所谓对内存的直接控制能力#xff0c;前文已经有所展示#xff1a;可以自行决定内存布局#xff0c;包括在栈上分配内存#xff0c;还是在堆上分配内存#xff1b;支…Rust语言的核心特点是在没有放弃对内存的直接控制力的情况下实现了内存安全。
所谓对内存的直接控制能力前文已经有所展示可以自行决定内存布局包括在栈上分配内存还是在堆上分配内存支持指针类型可以对一个变量实施取地址操作有确定性的内存释放等等。
另一方面从安全性的角度来说我们可以看到Rust有所有权概念、借用指针、生命周期分析等这些内容。
随便写个小程序都编译不通过学习曲线非常陡峭。那么,Rust设计者究竟是如何考虑的这个问题为什么要设计这样复杂的规则?Rust语言的这一系列安全规则背后的指导思想究竟是什么呢?
总的来说Rust的设计者们在一系列的“内存不安全”的问题中观察到了这样的一个结论 首先我们介绍一下这两个概念Alias和Mutation。
Alias的意思是“别名”。如果一个变量可以通过多种Path来访问那它们就可以互相看作alias。Alias意味着“共享”,我们可以通过多个入口访问同一块内存。Mutation的意思是“改变”。如果我们通过某个变量修改了一块内存就是发生了mutation。Mutation意味着拥有“修改”权限我们可以写入数据。
Rust保证内存安全的一个重要原则就是如果能保证alias和mutation不同时出现那么代码就一定是安全的。
编译错误示例
它们主要关心的是共享和可变之间的关系。“共享不可变可变不共享”是所有这些编译错误遵循的同样的法则。
下面我们通过几个简单的示例来直观地感受一下这个规则究竟是什么意思。
示例一 以上这段代码是可以编译通过的。其中变量绑定i、p1、p2指向的是同一个变量我们可以通过不同的Path访问同一块内存p,*p1,*p2,所以它们存在“共享”。而且它们都只有只读的权限所以它们存在“共享”,不存在“可变”。因此它一定是安全的。
示例二 我们让变量绑定i是可变的然后在存在p1的情况下通过i修改变量的值 编译出现了错误错误信息为 error:cannot assign to i’because it is borrowed [E0506] 这个错误可以这样理解在存在只读借用的情况下变量绑定i和p1已经互为alias,它们之间存在“共享”,因此必须避免“可变”。这段代码违反了“共享不可变”的原则。
示例三 如果我们把上例中的借用改为可变借用的话其实是可以通过它修改原来变量的值的。以下代码可以编译通过 那我们是不是说它违反了“共享不可变”的原则呢?其实不是。因为这段代码中不存在“共享”。在可变借用存在的时候编译器认为原来的变量绑定i已经被冻结(frozen),不可通过i读写变量。此时有且仅有p1这一个入口可以读写变量。证明如下 在pl存在的情况下不可通过i写变量。如果这种情况可以被允许那就会出现多个入口可以同时访问同一块内存且都具有写权限这就违反了Rust的“共享不可变可变不共享”的原则。错误信息为 error:cannot assign to i’because it is borrowed [E0506] 示例四 同时创建两个可变借用的情况 编译错误信息为 error:cannot borrow i’as mutable more than once at a time [E0499] 因为p1、p2都是可变借用它们都指向了同一个变量而且都有修改权限这是Rust不允许的情况因此这段代码无法编译通过。
正因如此mut型借用也经常被称为“独占指针”型借用也经常被称为“共享指针”。
内存不安全示例修改枚举
Rust设计的这个原则究竟有没有必要呢?它又是如何在实际代码中起到“内存安全”检查作用的呢? 第一个示例我们用enum来说明。假如我们有一个枚举类型 它有两个元素分别可以携带String类型的信息以及i64类型的信息。
假如我们有一个引用指向了它的内部数据同时再修改这个变量大家猜想会发生什么情况?这样做可能会出现内存安全问题因为我们有机会用一个String类型的指针指向i64类型的数据或者用一个i64类型的指针指向String类型的数据。完整示例如下 在这段代码中我们用if let语法创建了一个指向内部string的指针然后在此指针的生命周期内再把x内部数据变成i64类型。这是典型的内存不安全的场景。
幸运的是这段代码编译不通过错误信息为 error:cannot assign to x’because it is borrowed [E0506] 这个例子给了我们一个直观的感受为什么Rust需要“可变性和共享性不能同时存在”的规则?保证当前只有一个访问入口这是保证安全的可靠做法。
内存不安全示例迭代器失效
如果在遍历一个数据结构的过程中修改这个数据结构会导致迭代器失效。
在Rust里面下面这样的代码是不允许编译通过的 为什么 Rust可以避免这个问题呢?因为Rust里面的for循环实质上是生成了一个迭代器它一直持有一个指向容器的引用在迭代器的生命周期内任何对容器的修改都是无法编译通过的。类似这样 在整个for循环的范围内这个迭代器的生命周期都一直存在。
而它持有一个指向容器的引用型或者mut型根据情况而定。
迭代器的API设计是可以修改当前指向的元素没办法修改容器本身的。
当我们想在这里对容器进行修改的时候必然需要产生一个新的针对容器的mut型引用(clear方法的签名是Vec::clear(mut self),调用clear必然产生对原Vec的amut型引用)。这是与Rust的“aliasmutation”规则相冲突的所以编译不通过。
为什么在Rust中永远不会出现迭代器失效这样的错误?因为通过“mutationalias”规则就可以完全杜绝这样的现象这个规则是Rust内存安全的根是解决内存安全问题的灵魂。
Rust不是针对各式各样的场景用case by case的方式来解决内存安全问题。而是通过一种统一的机制高屋建瓴地解决这一类问题快刀斩乱麻直击要害。
这样的问题如果在编译阶段就能得到发现和解决才是最合适的解决方案。在遍历容器的时候同时对容器做修改可能出现在多线程场景也可能出现在单线程场景。
类似这样的问题依靠GC也没办法处理。GC只关心内存的分配和释放对于变量的读写权限是不关心的。GC在此处发挥不了什么作用。
而Rust依靠我们前面强调的“aliasmutation”规则就可以很好地解决该问题。这个思路的核心就是如果存在多个只读的引用是允许的如果存在可写的引用那么就一定不能同时存在其他的只读或者可写的引用。
大家看到这个逻辑是不是马上联想到多线程环境下的“ReadWriteLocker”?事实也确实如此。Rust检查内存安全的核心逻辑可以理解为一个在编译阶段执行的读写锁。多个读同时存在是可以的存在一个写的时候其他的读写都不能同时存在。
大家还记不记得Rust设计者总结的Rust的三大特点一是快二是内存安全三是免除数据竞争。
由上面的分析可见Rust所说的“免除数据竞争”,实际上和“内存安全”是一回事。
“免除数据竞争”可以看作多线程环境下的“内存安全”。单线程环境下的“内存安全”靠的是编译阶段的类似读写锁的机制与多线程环境下其他语言常用的读写锁机制并无太大区别。
也正因为Rust编译器在设计上打下的良好基础“内存安全”才能轻松地扩展到多线程环境下的“免除数据竞争”。
这两个概念其实只有一箭之隔。所以我们可以理解Java将此异常命名为“Concurrent”的真实含义——这里的“Concurrent”并不是单指多线程并发。
内存不安全示例悬空指针
我们再使用一个例子来继续说明为什么Rust的“mutationalias”规则是有必要的。我们这次通过制造一个悬空指针来解释。
同样使用动态数组类型使用一个指针指向它的第一个元素然后在原来的动态数组中插入数据 编译不通过错误信息为
error:cannot borrow arr’as mutable because it is also borrowed as immutable
我们可以看到“mutationalias”规则再次起了作用。在存在一个不可变引用的情况下我们不能修改原来变量的值。
写Rust代码的时候经常会有这样的感觉Rust编译器极其严格甚至到了“不近人情”的地步。
但是大部分时候却又发现它指出来的问题的确是对我们编程有益的。对它使用越熟练越觉得它是一个好帮手。