永远不要派人去做机器的工作。——史密斯探员
RAII代表“资源获取是初始化”;这是一种编程模式,其中值的生命周期与一些额外资源的生命周期完全相关。RAII模式由C++编程语言推广,是C++对编程的最大贡献之一。
使用RAII类型,
- 该类型的构造函数获取对某些资源的访问权限,并且
- 该类型的析构函数释放对该资源的访问权限。
其结果是,RAII类型具有不变性:当且仅当项目存在时,才能访问基础资源。由于编译器确保局部变量在作用域退出时被销毁,这反过来又意味着底层资源也在作用域退出1时被释放。
这对可维护性特别有帮助:如果随后对代码的更改改变了控制流,项目和资源寿命仍然正确。要看到这一点,请考虑一些手动锁定和解锁互斥体的代码;此代码在C++中,因为Rust的Mutex
不允许这种容易出错的使用!
// C++ code
class ThreadSafeInt {
public:
ThreadSafeInt(int v) : value_(v) {}
void add(int delta) {
mu_.lock();
// ... more code here
value_ += delta;
// ... more code here
mu_.unlock();
}
通过提前退出捕获错误条件的修改会使互斥量锁定:
// C++ code
void add_with_modification(int delta) {
mu_.lock();
// ... more code here
value_ += delta;
// Check for overflow.
if (value_ > MAX_INT) {
// Oops, forgot to unlock() before exit
return;
}
// ... more code here
mu_.unlock();
}
然而,将锁定行为封装到RAII类中:
// C++ code
class MutexLock {
public:
MutexLock(Mutex* mu) : mu_(mu) { mu_->lock(); }
~MutexLock() { mu_->unlock(); }
private:
Mutex* mu_;
};
意味着等效代码对这种修改是安全的:
// C++ code
void add_with_modification(int delta) {
MutexLock with_lock(&mu_);
// ... more code here
value_ += delta;
// Check for overflow.
if (value_ > MAX_INT) {
return; // Safe, with_lock unlocks on the way out
}
// ... more code here
}
在C++中,RAII模式最初通常用于内存管理,以确保手动分配(new
,malloc()
)和deallocation(delete
,free()
)操作保持同步。此内存管理的通用版本已添加到C++11的C++标准库中:thestdstd::unique_ptr<T>
类型确保单个地方拥有内存的“所有权”,但允许将指向内存的指针“借用”用于临时使用(ptr.get()
)。
在Rust中,内存指针的这种行为内置在语言中,但RAII的一般原则对其他类型的资源仍然有用2。为任何类型持有必须释放的资源的类型实现Drop
,例如:
- 访问操作系统资源。对于UNIX衍生的系统,这通常意味着包含文件描述符的东西;未能正确释放这些将保留系统资源(最终也会导致程序达到每个进程的文件描述符限制)。
- 访问同步资源。标准库已经包含内存同步原语,但其他资源(例如文件锁、数据库锁......)可能需要类似的封装。
- 访问原始内存,用于处理低级内存管理的
unsafe
类型(例如FFI)。
Rust标准库中最明显的RAII实例是Mutex::lock()操作返回的MutexGuard项,该项往往广泛用于使用第17项中讨论的共享状态并行性的程序。这大致类似于上面的最终C++示例,但在Rust中,MutexGuard
项目除了是持有锁的RAII项目外,还充当互斥保护数据的代理:
use std::sync::Mutex;
struct ThreadSafeInt {
value: Mutex<i32>,
}
impl ThreadSafeInt {
fn new(val: i32) -> Self {
Self {
value: Mutex::new(val),
}
}
fn add(&self, delta: i32) {
let mut v = self.value.lock().unwrap();
*v += delta;
}
}
建议不要为大部分代码保留锁;为了确保这一点,请使用块来限制RAII项目的范围。这导致略微奇怪的缩进,但为了增加安全性和寿命精度,这是值得的。
fn add_with_extras(&self, delta: i32) {
// ... more code here that doesn't need the lock
{
let mut v = self.value.lock().unwrap();
*v += delta;
}
// ... more code here that doesn't need the lock
}
在对RAII模式的使用进行了宣传后,对如何实施它的解释是有序的。Drop特征允许您将用户定义的行为添加到销毁项目时。此特征有一个单一的方法,即drop,编译器在保存该项目的内存释放之前运行该方法。
#[derive(Debug)]
struct MyStruct(i32);
impl Drop for MyStruct {
fn drop(&mut self) {
println!("Dropping {:?}", self);
}
}
drop
方法是专门为编译器保留的,不能手动调用,因为项目之后会处于潜在的混乱状态:
x.drop();
error[E0040]: explicit use of destructor method
--> raii/src/main.rs:65:7
|
65 | x.drop();
| --^^^^--
| | |
| | explicit destructor calls not allowed
| help: consider using `drop` function: `drop(x)`
(根据编译器的建议,只需调用drop(obj)即可手动删除项目,或将其包含在上述建议的范围内。)
因此,drop
方法是实现RAII模式的关键场所;其实现是释放相关资源的理想场所。
1:这也意味着RAII作为一种技术,主要仅在具有可预测销毁时间的语言中可用,这排除了大多数垃圾收集的语言(尽管Go的defer语句实现了一些相同的目的。)