在Rust中实现类似Golang中的defer功能

huangapple go评论84阅读模式
英文:

Golang-like defer in Rust

问题

在Go语言中,可以使用defer关键字在当前函数返回时执行一个函数,类似于其他语言中的传统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,但在我的特定情况下,状态是在外部系统中。我正在编写一个测试,将一个键写入键值存储,并且我需要确保在测试结束时删除它,无论测试中的断言是否导致恐慌。

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

还有一个关于Rust GitHub存储库的这个问题,但它已经三年了,显然不再相关。

英文:

In Go, you can use the defer keyword to execute a function when the current function returns, similar to the traditional finally keyword in other languages. This is useful for cleaning up state regardless of what happens throughout the function body. Here's an example from the Go blog:

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)
}

How can this functionality be achieved in Rust? I know about RAII, but in my specific case the state is in an external system. I'm writing a test that writes a key to a key-value store, and I need to make sure it's deleted at the end of the test regardless of whether or not the assertions in the test cause a panic.

I found this Gist but I don't know if this is a recommended approach. The unsafe destructor is worrisome.

There is also this issue on the Rust GitHub repository, but it's three years old and clearly not very relevant anymore.

答案1

得分: 48

(e: 不要错过bluss的答案和他们的scopedguard crate。)

正确的方法是通过在析构函数中运行代码来实现,就像你链接的defer!宏所做的那样。对于任何不仅仅是临时测试的情况,我建议编写一个具有适当析构函数的句柄类型,例如通过其MutexGuard类型(由lock返回)与std::sync::Mutex进行交互:不需要在互斥锁本身上调用unlock。(显式的句柄-析构函数方法也更加灵活:它具有对数据的可变访问,而延迟的方法可能无法访问,因为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")允许在有多个语句时不使用内部的{ ... }来调用它(即它的行为更像一个“正常”的控制流结构):

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()()
    }
}

这也需要使用Some将值包装到c中来构造ScopeCall。由于调用FnOnce会移动所有权,而在self: &mut ScopeCall<F>后面无法进行所有权移动,所以需要进行Option的操作。(这样做是可以的,因为析构函数只执行一次。)

总而言之:

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);
}

(与原始代码输出相同。)

<details>
<summary>英文:</summary>

(*e:* don&#39;t miss bluss&#39;s answer and their [scopedguard](https://crates.io/crates/scopeguard) crate, below.)

The correct way to achieve this by having code that runs in a destructor, like the `defer!` macro you link to does. For anything more than ad-hoc testing I would recommend writing a handle type with a proper destructor, e.g. one interacts with `std::sync::Mutex` via its [`MutexGuard`](http://doc.rust-lang.org/nightly/std/sync/struct.MutexGuard.html) type (returned by `lock`): there&#39;s no need to call `unlock` on the mutex itself. (The explicit handle-with-destructor approach is more flexible too: it has mutable access to the data, whereas the deferred approach may not be able to, due to Rust&#39;s strong aliasing controls.)

In any case, that macro is now (much!) improved due to recent changes, in particular, pnkfelix&#39;s [sound generic drop work](https://github.com/rust-lang/rfcs/blob/master/text/0769-sound-generic-drop.md), which removes the necessity for `#[unsafe_destructor]`. The direct update would be:

    struct ScopeCall&lt;F: FnMut()&gt; {
        c: F
    }
    impl&lt;F: FnMut()&gt; Drop for ScopeCall&lt;F&gt; {
        fn drop(&amp;mut self) {
            (self.c)();
        }
    }
     
    macro_rules! defer {
        ($e:expr) =&gt; (
            let _scope_call = ScopeCall { c: || -&gt; () { $e; } };
        )
    }
     
    fn main() {
        let x = 42u8;
        defer!(println!(&quot;defer 1&quot;));
        defer!({
            println!(&quot;defer 2&quot;);
            println!(&quot;inside defer {}&quot;, x)
        });
        println!(&quot;normal execution {}&quot;, x);
    }

Output:

    normal execution 42
    defer 2
    inside defer 42
    defer 1

--- 

Although, it would be syntactically nicer as:

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

(The `tt hack` is necessary due to [#5846](https://github.com/rust-lang/rust/issues/5846).)

The use of the generic `tt` (&quot;token tree&quot;) allows one to invoke it without the inner `{ ... }` when there are multiple statements (i.e. it behaves more like a &quot;normal&quot; control flow structure):

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

Also, for maximum flexibility about what the deferred code can do with captured variables, one could use `FnOnce` instead of `FnMut`:

    struct ScopeCall&lt;F: FnOnce()&gt; {
        c: Option&lt;F&gt;
    }
    impl&lt;F: FnOnce()&gt; Drop for ScopeCall&lt;F&gt; {
        fn drop(&amp;mut self) {
            self.c.take().unwrap()()
        }
    }

That will also require constructing the `ScopeCall` with a `Some` around the value for `c`. The `Option` dance is required because calling a `FnOnce` moves ownership, which isn&#39;t possible from behind `self: &amp;mut ScopeCall&lt;F&gt;` without it. (Doing this is OK, since the destructor only executes once.)

All in all:

    struct ScopeCall&lt;F: FnOnce()&gt; {
        c: Option&lt;F&gt;
    }
    impl&lt;F: FnOnce()&gt; Drop for ScopeCall&lt;F&gt; {
        fn drop(&amp;mut self) {
            self.c.take().unwrap()()
        }
    }
 
    macro_rules! expr { ($e: expr) =&gt; { $e } } // tt hack
    macro_rules! defer {
        ($($data: tt)*) =&gt; (
            let _scope_call = ScopeCall {
                c: Some(|| -&gt; () { expr!({ $($data)* }) })
            };
        )
    }
    
    fn main() {
        let x = 42u8;
        defer!(println!(&quot;defer 1&quot;));
        defer! {
            println!(&quot;defer 2&quot;);
            println!(&quot;inside defer {}&quot;, x)
        }
        println!(&quot;normal execution {}&quot;, x);
    }

(Same output as the original.)


</details>



# 答案2
**得分**: 27

我使用以下代码作为作用域保护器。它使用`Deref`特性来提供对受保护值的共享和可变访问,而不会移动它(这会使保护器失效!)

我的使用案例是在程序退出时正确重置终端,即使发生恐慌:

```rust
extern crate scopeguard;
use scopeguard::guard;

// ... 省略终端设置 ...

// 使用作用域保护器在退出/恐慌时恢复终端设置
let mut tty = guard(tty, |tty| {
    // ... 这里也使用 tty.write() ...
    ts::tcsetattr(tty.as_raw_fd(), ts::TCSANOW, &old_attr).ok();
});
game_main(&mut tty).unwrap();   // Deref 强制转换魔法在这里将内部的 &mut TTY 指针传递出去。

模块 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)
    }
}

现在这个 crate 可以在 crates.io 上找到。

英文:

I use the following for a scope guard. It uses the Deref traits to provide shared & mutable access to the guarded value, without moving it out (that would invalidate the guard!)

My use case is correctly resetting the terminal when the program exits, even if panicking:

<!-- language: lang-rust -->

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, &amp;old_attr).ok();
});
game_main(&amp;mut tty).unwrap();   // Deref coercion magic hands off the inner &amp;mut TTY pointer here.

Module scopeguard.rs:

<!-- language: lang-rust -->

use std::ops::{Deref, DerefMut};
pub struct Guard&lt;T, F&gt; where
F: FnMut(&amp;mut T)
{
__dropfn: F,
__value: T,
}
pub fn guard&lt;T, F&gt;(v: T, dropfn: F) -&gt; Guard&lt;T, F&gt; where
F: FnMut(&amp;mut T)
{
Guard{__value: v, __dropfn: dropfn}
}
impl&lt;T, F&gt; Deref for Guard&lt;T, F&gt; where
F: FnMut(&amp;mut T)
{
type Target = T;
fn deref(&amp;self) -&gt; &amp;T
{
&amp;self.__value
}
}
impl&lt;T, F&gt; DerefMut for Guard&lt;T, F&gt; where
F: FnMut(&amp;mut T)
{
fn deref_mut(&amp;mut self) -&gt; &amp;mut T
{
&amp;mut self.__value
}
}
impl&lt;T, F&gt; Drop for Guard&lt;T, F&gt; where
F: FnMut(&amp;mut T)
{
fn drop(&amp;mut self) {
(self.__dropfn)(&amp;mut self.__value)
}
}

This is now the crate scopeguard on crates.io.

huangapple
  • 本文由 发表于 2015年4月30日 17:04:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/29963449.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定