Rust使用所有权这一概念来管理对象的生命周期。而在这里面需要提到两个主要概念:一是RAII,二是资源所有权。
RAII是被C++之父提出,用于管理C++中诸如堆内存等资源的。原理是在C++类实例被销毁后,会调用其析构函数,该过程由编译器保证必定会实行。故而可以借由析构函数的销毁,用于释放资源:
class RAII {
public:
RAII() {
mpStr = new std::string("RAII");
}
~RAII() {
if(mpStr) {
std::cout << "delete :" << *mpStr << std::endl;
delete mpStr;
}
};
private:
std::string* mpStr;
};
int main() {
RAII i;
}
i作为一个局部定义的变量,被存储在栈上,当离开作用域时被销毁,由于析构函数必然会被调用,故而释放类实例中申请的堆内存。
引申到Rust,可以这么认为,所有需要在申请堆内存的资源,都是自动通过RAII管理的。诸如String,Vec这样需要可变空间的类型。
fn main() {
let s = String::from("hello");
}
该字符串s在离开作用域后调用一个drop()函数用于释放分配的堆内存,类似于C++中在析构函数所作的那样。也因而,Rust也可以通过智能指针这样的套件去延长对象的声明周期。使用RAII来管理资源(包括使用引用计数的智能指针),核心在于节省运行期回收内存资源开销,并且能大概率保证资源在不被使用的时候就会被释放。当然这里存在两个例外,其一是代码不规范,导致资源在错误的节点被分配,所以一般习惯是在需要资源的时候在进行分配(或者可以将资源分配放到一个独立的init函数进行,需要的时候调用,但释放仍由析构执行);二是使用引用计数的智能指针有可能导致循环引用,使得资源无法释放,注意配合与强引用指针对应的弱引用指针一起使用。
Rust的第二个概念是所有权。换成C++的方式表述,Rust绝大部分的值都是默认只移型别,只有少部分诸如基本类型,才支持默认拷贝。也就是说,写下这么一段语句:
let s = String::from("hello");
let s2 = s;
在C++中,等同于:
std::string s1 {"hello"};
std::string s2 = std::move(s1);
在Rust中,除了基本类型以外,赋值都是移动,这样变量就唯一拥有了其指向资源的所有权,使得并发编程的资源争用风险大大减少。如果想要赋值操作表现出拷贝行为,则需要显式调用clone成员。
上面还有一个坑是,C++在资源所有权转移之后,理论上资源所有权被"抢夺"了的变量,仍然是可以被访问的,比如上文的那两个字符串,打印s1会获得一个空字符串。我觉得这个多少算是一个编译期可以解决的问题吧,Rust的话,使用被移动后的对象会直接编译不过,感觉这样更合理一些。