今天学习Rust中的所有权系统。按照官方教程所述,所有权系统包含三个部分:
- 所有权
- 借用
- 生命周期
看完发现,C++吭哧吭哧从99发展到11、14、17标准,Rust早就在山顶等它了。下面还是流水账记录心得体会。
Rust利用移动语义 (Move semantics) 确保: 任何给定的资源正好只有一个绑定与之对应。教程中有个例子解释了这一点:
let v = vec![1,2,3];
let v2 = v;
println!("v[0] is : {}", v[0]);
这个例子是无法编译通过的。其原因是当用v2绑定v的时候,v这个绑定就自动失效了,v原本的资源转移到了v2。这个效果和c++11早期的智能指针auto_ptr很像。当auto_ptr拷贝赋值给另一个auto_ptr时,原指针会被置为null。
当时因为auto_ptr这个特性踩过坑,后来auto_ptr被unique_ptr替代,换成了更温和的std::move语句,要求用户显式调用move语句来完成资源所有权的转移。现在看来,C++当时可能的确是想对指针有更严格的控制,可惜抵挡不住舆论的压力,还是怂了。扯远了,继续回来学习。
默认的移动语义带来的问题是,当我想在绑定后仍然访问原变量值,就必须手动重新绑定。不想那么麻烦那就要用到 所有权系统 的第二个特性,借用。借用是通过两种引用方式以及一系列必须遵守的规则来实现的。
引用方式包括, &T 和 &mut T,前者为不可变引用,后者为可变引用。
-
规则:
1. 任何借用的作用域必须小于原变量的作用域。
2. 可变引用和不可变引用不能同时出现;可以同时存在多个不可变引用,但只能同时存在一个可变引用。
3. 引用必须与它引用的值存活得一样长。
这些规则的遵守是由编译器来保证的。看到这个规则我就忍不住拍案感叹,C++中数据竞争,以及多少靠摸索总结出来的编码规范在Rust中已经是标配,而且不遵守还不行,服!在C++中,失效指针,失效引用处理起来都非常头疼,难以定位问题,其问题的根源也就是“引用和被引用的值并未存活得一样长”。举例来看:
let y: &i32;
{
let x = 5;
y = &x;
}
println!("{}", y);
当离开大括号区域后,x绑定的资源会被释放,如果y还绑定着x,且后续还有对y的访问操作,那势必会造成崩溃。幸运得是,Rust不会让这段程序通过编译。因为y在x之前申明,也就是意味着,y比x的生命周期更长,这不符合绑定的规则。
在C++中也有生命周期的概念,比如static是全局生命周期,栈上分配的变量只存在于外层大括号所在区域。堆上分配的变量取决于用户何时申请和释放。在Rust中可以给变量显式地指明生命周期。