#raii #error-handling #scopeguard

no-std stated-scope-guard

一种更灵活的声明资源管理的 RAII 模式

1 个不稳定版本

0.1.0 2024 年 5 月 8 日

#1375 in Rust 模式

MIT/Apache

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,状态可以是AllThingsGoRightSomethingWrong。如果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的回调中检查状态,实际上,根本没有任何状态变量暴露给用户。

无运行时依赖