改进rust代码的35种具体方法-类型(十一)-RAII模式的Drop特征

上一篇文章


永远不要派人去做机器的工作。——史密斯探员

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模式最初通常用于内存管理,以确保手动分配(newmalloc())和deallocation(deletefree())操作保持同步。此内存管理的通用版本已添加到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语句实现了一些相同的目的。)

2:RAII对于低级unsafe代码中的内存管理仍然有用,但这(大多)超出了本书的范围

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值