1 个不稳定版本
0.1.0 | 2024 年 5 月 8 日 |
---|
#1375 in Rust 模式
11KB
74 行
声明作用域保护
作用域保护是 RAII(资源获取即初始化)的实际应用,用于避免资源泄露,声明作用域保护是一种更灵活的声明资源管理 RAII 模式。
要使用此crate,只需在 Cargo.toml
中添加以下内容:
[dependencies]
stated-scope-guard = "0.1"
背景
如果您熟悉 RAII,可以跳过此部分。
对于支持构造函数和析构函数的编程语言(对于 Rust,是 new()
和 drop()
),资源可以在构造函数和析构函数内进行管理,以避免资源泄露。例如,在 POSIX 环境中,文件可以通过 libc 的 open
函数打开,并由 libc 的 close
函数关闭。如果一个文件被打开后没有关闭,它可能在一个长时间运行的应用程序中导致资源泄露。为了解决这个问题,我们可以将文件描述符封装在 Rust 结构体 File
中。要创建 File
实例,应调用其 new
方法,在该方法中,调用 libc 的 open
函数,并将文件描述符存储在 File
结构体中。当 File
实例离开其作用域时,其 drop
方法将被自动调用,在该方法中,使用存储的文件描述符调用 libc 的 close
函数。这种模式称为 RAII,或作用域保护。
作用域保护最好的地方是让编译器负责确保资源得到妥善管理。资源管理在旧时代的 C 中可能会让开发者发狂,例如
void every_fault_shall_be_handled(char *path) {
int fd = open(path, O_RDONLY);
if (fd == OPEN_FAILED) { return; }
if (file_op_may_fail(fd) == FAILED) { close(fd); return; }
int sock = socket(/* ... */);
if (sock == OPEN_FAILED) { close(fd); return; }
if (send_file_to_sock(fd, sock) == FAILED) {
close(fd);
close(sock);
return;
}
close(sock);
close(fd);
}
在处理错误时,必须在每次 return
之前小心地处理资源,在大型的项目如 Linux 内核中,他们倾向于使用 goto err
进行资源清理。
在 C++/Rust/Python/Java/... 中,每个 return
都意味着作用域的结束,这将自动调用剩余实例的析构函数,资源就这样被释放,开发者无需做任何事情,太棒了!
错误处理的声明资源管理
在需要显式资源管理时,事情会变得更加复杂。让我们考虑以下情况:如果任何步骤失败,资源应该被回滚;而如果所有步骤都成功,即使程序退出,资源也应该被保留(不回滚)。例如
fn setup() -> anyhow::Result<()> {
let log_dir = LogDir::create()?;
let user_account = UserAccount::create().inspect_err(|_| {
delete_log_dir(log_dir);
})?;
let network = UserNetwork::create().inspect_err(|_|) {
delete_user_account(user_account);
delete_log_dir(log_dir);
}?;
Ok(())
}
在这种情况下,传统的作用域保护无法按预期工作,因为像logdir
这样的资源需要在所有事情都顺利进行时才始终被删除。这个问题可以进一步扩展为显式资源管理,即根据函数的状态以不同的方式管理资源。对于logdir
,状态可以是AllThingsGoRight
或SomethingWrong
。如果SomethingWrong
,我们应该删除logdir
,而当AllThingsGoRight
时,我们不需要删除logdir
。现在,如果有很多状态和很多资源要处理,我们该怎么办呢?
用法
为了解决显式资源管理,我们可以像这样使用stated-scope-guard
存储库
use stated_scope_guard::ScopeGuard;
struct Resource;
impl Resource { fn new() -> Self { Self }}
// Define the state enumerate
enum State {
State1,
State2,
// ...
}
fn setup() {
// The resource guard can be dereferenced into the resource passed in,
// the callback will be called when resource_guard is dropped. The callback
// is expected to deal with resource differently according to the state.
let mut resource_guard = ScopeGuard::new(Resource::new(), State::State1, |res, state| {
match state {
State::State1 => { /* do something with res */ },
State::State2 => { /* do something else with res */ },
// ...
}
});
// do something may throw.
// ...
// When throwed, the resource guard will deal with res with state State1
resource_guard.set_state(State::State2);
// After this, when resource guard leaves its scope, the resource will be
// dealt with state State2
}
传递给ScopeGuard
的第三个参数是一个回调,当作用域保护被丢弃时会调用。它接受当前资源和状态作为参数,并期望根据状态处理资源。当需要改变状态时,我们可以使用set_state
来这样做。
可解散的作用域保护
对于更常见且简单的情况,其中只有两种状态,默认状态动作是回滚,另一种是不做任何事情,这正是上面提到的logdir
的情况,我们提供了DismissibleScopeGuard
,我们可以这样使用它
use stated_scope_guard::dismissible::new_dismissible;
let mut log_dir_guard = new_dismissible(LogDir::new(), |log_dir| {
delete_log_dir(log_dir);
});
do_something()?;
log_dir_guard.dismiss();
Ok(())
调用dismiss
后,回调将永远不会被执行,所以我们不需要在传递给new_dismissible
的回调中检查状态,实际上,根本没有任何状态变量暴露给用户。