0x00 开篇
不知道大家对前一篇文章的概念搞懂了没有呢?本篇文章将通过与 Python、C++语言对比来解释移动语义。本篇文章的阅读时间大约 10 分钟。
0x01 变量绑定与所有权
我们在声明变量时,使用的是 let
关键字,这里的变量,本质上是一种绑定关系。let
将一个变量和值绑定在一起,变量则对这个值拥有所有权。对值拥有所有权可能说的有点儿模糊,实际是应该说 let
的作用是将变量与值的内存空间进行绑定,使得变量对值所在的内存空间拥有所有权。这就会出现对于每一块内容空间都拥有为一个绑定变量与其对应,非特殊场景下不允许多个变量同时指向同一块内存空间。
PS:特殊场景指的是 共享所有权
,后面的章节会讲到,请先忽略这个概念。
0x02 转移(Move)
在 Rust 中,在一些需要在堆上保存数据的类型来说,对于变量之间赋值,向函数传值,从函数返回值这些操作不会复制值,而是转移(move)值,将原来值所有者的所有权让渡给目标所有者。对于原来的所有者将变成不可访问状态。这也是 Rust 与其它语言的不同之处,如果你能理解 move,那表明你已经掌握了 Rust 三分之一的内容了。所有权机制仅仅是针对在堆上分配数据的类型。下面将对这三个场景展开详细介绍。
0x03 转移——变量之间赋值
先展示个基本类型和 String
类型变量间赋值的示例吧。示例代码如下:
fn main() { // 基本数据类型 let a = 3; let b = a; println!("a = {}, b = {}", a, b); // String类型 let m = String::from("rust"); // 让渡所有权 let n = m; // m 变为无法访问,下面的代码将会报错 // println!("m = {}, n = {}", m, n); println!("n = {}", n); // 变量遮蔽 let m = 5; println!("m = {}", m); } // 运行结果 // a = 3, b = 3 // n = rust // m = 5
当我打开 println!("m = {}, n = {}", m, n);
这行代码的注释,编译将出现下面的错误。
蓝字大致意思:因为变量 m
是一个 String
类型,该类型并不会实现 Copy
trait,在这里发生了 move 。发生转移的代码是 let n = m
。一旦发生转移,变量 m
将无法访问。当然我们可以通过变量遮蔽的特性,重新为 m
赋新值。
下面我将通过内存分析与 Python、 C++ 比较,Rust 与他们的不同之处。
假设有一个列表 a, 包含三个字符串 "hello", "study", "rust"。将此列表赋值给 b,再赋值给 c。
Python
Python 代码如下: a = ['hello', 'stduy', 'rust'] b = a c = a
Python变量赋值内存简易模型
上图是 Python 变量赋值内存简易模型,忽略了一些非必要字段。由图可见,每一个 Python 对象都存在一个引用计数,用来记录当前引用此值的数量。上图左边表示变量 b
,c
赋值之前,内存中的列表的引用计数为1。当被赋值之后(上图右侧标红显示),Python 对于变量间赋值,是创建一个指向对象的新引用,然后增加引用计数。
C++
我们再来看下 C++,代码如下:
vector<string> a = {"hello", "study", "rust"};
vector<string> b = a;
vector<string> c = a;
C++变量赋值内存简易模型
上图是 C++ 变量赋值内存简易模型,忽略了一些非必要字段。赋值前(上图左边)栈上保存指向堆上数据的指针。赋值后(上图右边)内存中会出现 3 个 vector 和 9 个字符串。由上图可得,C++ 的赋值是采用了深复制的做法。显而易见 C++ 的赋值代价是比较大的,可能会消耗较多的内存和处理器时间。但是它的优点就是所有权非常清晰,在变量超出作用域,我们就可以直接释放内存。
Rust
最后再来看 Rust,Rust 则采用了相反的策略,这个在本篇文章开始已经讲过了,现在来看下内存模型吧。示例代码如下:
let a = vec![String::from("hello"), String::from("study"), String::from("rust")]; let b = a; // 下面的语句会报错 // let c = a;
Rust 变量赋值内存简易模型
经过 let b = a
后,把向量 a
的三个字段转移到了 b
上,b
则拥有了这个向量,向量原有在堆上的数据并没有移动。向量的所有者还是只有一个。原有的向量 a
则不能被使用,编译器认为其没有被初始化。这个过程没有发生堆上的数据移动,也没有引用计数,代价非常小。这种方式有没有发现很巧妙呢,哈哈。
0x04 小结
Rust 通过与众不同的方式确保了内存安全,所有权系统也是 Rust 的核心功能。文章通过对比 Python
和 C++
,其实也是对比了两种方式,目前主流虚拟机语言大部分都采用引用计数器方法。其实语言内存的管理方法都各有优劣,并不能说 Rust 转移这种做法就没有缺点,比如使用 Rust 来实现很多数据结构有难度等等,毕竟为了性能总需要有点儿取舍。希望你看完本篇文章能对 Rust 有一个新的认识。