0x00 开篇
从今天起开启 Rust 的重头戏,前面的知识可能对于一些拥有编程基础的读者们来说很简单,因为基本上都大同小异,但是所有权这个是 Rust 的新知识点。本篇文章将告诉你 Rust 为什么安全以及介绍一些通用的概念。本篇文章的阅读时间大约 6 分钟。
0x01 Rust 为什么安全
为了保证安全性,Rust 有两个承诺:
- 开发者决定程序中值的生存时长。
Rust 将会在你的控制下迅速释放与某个值关联的内容和其它资源。 - 开发者将永远不会在一个对象释放后还使用指向它的指针。
不会出现悬垂指针的问题。在程序编译期,Rust 将会捕获到这些错误。
悬垂指针:指针所指向的对象已经被释放或者回收了,但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。此类指针称为垂悬指针。
那么其它语言是如何处理的呢?
C/C++
C/C++ 仅仅是遵循了第一个原则,所以你常会在 C/C++ 中遇到悬垂指针。说实话,遇到程序崩溃算是运气不错,悬垂指针很容易出现重大的安全漏洞问题。所以在使用 C/C++ 时,我们要非常细心。
Java/C#
很多高级语言为了实现第二个原则,引入了垃圾回收机制。垃圾回收机制是当指向某个对象的所有指针都失效时会自动释放该对象。所以这些语言没有遵循第一个原则,对象释放的控制权交由了垃圾回收器(其实也可以主动调用回收,开发者一般不会主动调用)。这种做法的优点就是开发者在使用时可以专注业务,这也是 Java/Go 流行的原因之一了。
但是 Rust 不接受任何一项妥协,它打破了常规,另寻出路,采用了一种特别的方式——限制程序使用指针的方式。这也是为什么网络上充斥着各种以下好笑的言论:Rust 学习周期陡峭
,学习 Rust 要跟编译器作斗争
等等吧。其实这都是为了安全。在介绍所有权前,我们先来了解几个语言里的通用概念。
0x02 深复制与浅复制
上篇文章,我们了解了栈和堆。而深拷贝和浅拷贝就与栈和堆有关系。
- 浅复制:也称作浅拷贝。是指复制栈上的数据,所以有时也称作栈复制。
在 Rust 中,基本的数据类型都支持浅复制。通俗点说,数据只存储在栈上的数据类型都默认支持浅复制。
示例代码:
fn main() { // 1. 值语义 let a = 8; let mut b = a; b = 10; println!("a = {}, b = {}", a, b); }
- 深复制:也称作深拷贝。是指复制栈上的数据和堆上的数据。
在 Rust 中,字符串类型,数组,向量,胖指针等在栈上和堆上都存在数据的类型,我们需要进行深复制。
0x03 值语义(Value Semantics)
值语义,又叫值类型语义(Value-Type Semantics)或者按值复制语义(Copy-By-Value Semantics)。值语义是指按位复制后与原值无关,保证值的独立性,唯一性。在大部分语言中的基本数据类型都是值语义。按位复制就是栈复制。通俗一点讲,具有值语义的对象只能由赋值运算符修改。我们可以根据需要复制它,且任何副本都可以代替原始副本,而不会改变程序的行为。对于一个对象,只有对象的值是重要的,它的标识并不重要。
0x04 引用语义(Reference Semantics)
与值语义相对应的是引用语义。引用语义一般是指数据存储在堆内存中,通过栈内存里的指针来管理内存的数据(网络上找到的定义)。以 Rust 的向量(我们以及在《RUST 学习日记 第12课 ——切片(Slice)》
介绍过向量的存储方式)为例,声明向量时,有一个区域是指向堆区的指针,而真正的数据则来自指针指向堆区的数据。可以按位复制(浅复制),但是按位复制会导致一个栈上会有多个指针指向堆上的同一区域,释放指针时容易造成堆上的数据二次释放。如果要复制引用语义上的数据通常是采用深复制,将栈上和堆上的数据同时复制。
0x05 所有权机制
上面两个语义,基本所有的语言都会存在。在此基础上,Rust 增加了另外两个语义——复制语义(Copy Semantics)和移动语义(Move Semantics)。这算是 Rust 对值语义与引用语义的重新包装。复制语义对应值语义,移动语义对应引用语义。但是在所有权的机制下,又存在一些不同。
- 复制语义:可以在栈上进行安全的按位复制的类型。
- 移动语义:对于在堆上存储的数据,在栈上按位复制数据时,会导致多指针指向同一堆上的数据,容易引发内存问题。所以要解决这个问题,我们必须使用深复制。但是深复制时,需要开闭新的内存空间,又带了性能问题。所以我们可以保持堆上的数据不变,我们只需要移动指向堆内存的指针即可。
0x06 小结
本篇文章主要介绍了一些概念吧,有些读者也许看完文章会感觉到很懵,Rust 提出的移动语义也许你在其他任何语言中都没有见过。接下来的文章也将会告诉你为什么 Rust 要提出移动语义的概念。