Rust 中类似 Golang 的延迟

Golang-like defer in Rust

在Go中,可以使用defer关键字在当前函数returns时执行一个函数,类似于其他语言中传统的finally关键字。这对于清理状态很有用,无论整个函数体发生了什么。这是来自 Go 博客的示例:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

如何在 Rust 中实现此功能?我知道 RAII,但在我的具体情况下,状态在外部系统中。我正在编写一个将键写入键值存储的测试,我需要确保它在测试结束时被删除,无论测试中的断言是否导致恐慌。

我找到了 this Gist,但我不知道这是否是推荐的方法。不安全的析构函数令人担忧。

在 Rust GitHub 存储库中也有 this issue,但它已有三年历史,显然不再相关。

(e: 不要错过下面 bluss 的回答和他们的 scopedguard 箱子。)

实现此目的的正确方法是让代码在析构函数中运行,就像您 link 执行的 defer! 宏一样。除了临时测试之外,我建议编写一个带有适当析构函数的句柄类型,例如一个通过其 MutexGuard 类型(由 lock 返回)与 std::sync::Mutex 交互:不需要在互斥量本身上调用 unlock。 (显式 handle-with-destructor 方法也更灵活:它具有对数据的可变访问权限,而由于 Rust 强大的别名控制,延迟方法可能无法访问。)

无论如何,由于最近的更改,特别是 pnkfelix 的 sound generic drop work,该宏现在(大大!)得到了改进,这消除了 #[unsafe_destructor] 的必要性。直接更新为:

struct ScopeCall<F: FnMut()> {
    c: F
}
impl<F: FnMut()> Drop for ScopeCall<F> {
    fn drop(&mut self) {
        (self.c)();
    }
}

macro_rules! defer {
    ($e:expr) => (
        let _scope_call = ScopeCall { c: || -> () { $e; } };
    )
}

fn main() {
    let x = 42u8;
    defer!(println!("defer 1"));
    defer!({
        println!("defer 2");
        println!("inside defer {}", x)
    });
    println!("normal execution {}", x);
}

输出:

normal execution 42
defer 2
inside defer 42
defer 1

尽管如此,它在语法上会更好:

macro_rules! expr { ($e: expr) => { $e } } // tt hack
macro_rules! defer {
    ($($data: tt)*) => (
        let _scope_call = ScopeCall { 
            c: || -> () { expr!({ $($data)* }) }
        };
    )
}

(由于 #5846tt hack 是必需的。)

通用 tt ("token tree") 的使用允许在有多个语句时调用它而无需内部 { ... } (即它的行为更像 "normal" 控制流结构):

defer! {
    println!("defer 2");
    println!("inside defer {}", x)
}

此外,为了最大限度地灵活地处理延迟代码可以对捕获的变量执行的操作,可以使用 FnOnce 而不是 FnMut:

struct ScopeCall<F: FnOnce()> {
    c: Option<F>
}
impl<F: FnOnce()> Drop for ScopeCall<F> {
    fn drop(&mut self) {
        self.c.take().unwrap()()
    }
}

这还需要在 c 的值周围使用 Some 构建 ScopeCallOption 舞蹈是必需的,因为调用 FnOnce 会移动所有权,如果没有它,这是不可能从 self: &mut ScopeCall<F> 后面进行的。 (这样做是可以的,因为析构函数只执行一次。)

总而言之:

struct ScopeCall<F: FnOnce()> {
    c: Option<F>
}
impl<F: FnOnce()> Drop for ScopeCall<F> {
    fn drop(&mut self) {
        self.c.take().unwrap()()
    }
}

macro_rules! expr { ($e: expr) => { $e } } // tt hack
macro_rules! defer {
    ($($data: tt)*) => (
        let _scope_call = ScopeCall {
            c: Some(|| -> () { expr!({ $($data)* }) })
        };
    )
}

fn main() {
    let x = 42u8;
    defer!(println!("defer 1"));
    defer! {
        println!("defer 2");
        println!("inside defer {}", x)
    }
    println!("normal execution {}", x);
}

(与原始输出相同。)

我将以下内容用于范围保护。它使用 Deref 特征来提供对受保护值的共享和可变访问,而无需将其移出(这会使保护无效!)

我的用例是在程序退出时正确重置终端,即使出现恐慌也是如此:

extern crate scopeguard;
use scopeguard::guard;

// ... terminal setup omitted ...

// Use a scope guard to restore terminal settings on quit/panic
let mut tty = guard(tty, |tty| {
    // ... I use tty.write() here too ...
    ts::tcsetattr(tty.as_raw_fd(), ts::TCSANOW, &old_attr).ok();
});
game_main(&mut tty).unwrap();   // Deref coercion magic hands off the inner &mut TTY pointer here.

模块scopeguard.rs:

use std::ops::{Deref, DerefMut};

pub struct Guard<T, F> where
    F: FnMut(&mut T)
{
    __dropfn: F,
    __value: T,
}

pub fn guard<T, F>(v: T, dropfn: F) -> Guard<T, F> where
    F: FnMut(&mut T)
{
    Guard{__value: v, __dropfn: dropfn}
}

impl<T, F> Deref for Guard<T, F> where
    F: FnMut(&mut T)
{
    type Target = T;
    fn deref(&self) -> &T
    {
        &self.__value
    }

}

impl<T, F> DerefMut for Guard<T, F> where
    F: FnMut(&mut T)
{
    fn deref_mut(&mut self) -> &mut T
    {
        &mut self.__value
    }
}

impl<T, F> Drop for Guard<T, F> where
    F: FnMut(&mut T)
{
    fn drop(&mut self) {
        (self.__dropfn)(&mut self.__value)
    }
}

这就是箱子 scopeguard on crates.io.