如何在不丢失对象的情况下解构它?

7
我有一个结构体需要按值传递、改变然后返回。我还想改变它的泛型类型,因为我使用这个状态来静态地确保函数调用的正确顺序,以实现安全的 FFI。 (示例:playground
use core::marker::PhantomData;

struct State1 {}
struct State2 {}
struct Whatever {}

struct X<State> {
    a: Whatever,
    b: Whatever,
    c: Whatever,
    _d: PhantomData<State>,
}

impl<State> Drop for X<State> {
    fn drop(&mut self) {}
}

fn f(x: X<State1>) -> X<State2> {
    let X { a, b, c, _d } = x;
    //mutate a, b and c
    X {
        a,
        b,
        c,
        _d: PhantomData,
    } // return new instance
}

因为 X 实现了 Drop,所以我得到:
error[E0509]: cannot move out of type `X<State1>`, which implements the `Drop` trait
  --> src/lib.rs:19:29
   |
19 |     let X { a, b, c, _d } = x;
   |             -  -  -         ^ cannot move out of here
   |             |  |  |
   |             |  |  ...and here
   |             |  ...and here
   |             data moved here
   |
   = note: move occurs because these variables have types that don't implement the `Copy` trait

我不想丢弃任何东西,因为并没有销毁x,只是重新打包它。防止丢失x的惯用方式是什么?


@alagris,我误解了你最初的问题,我的回答忽略了你所问的主要问题。Shepmaster的编辑使问题更加清晰明了。 - Peter Hall
恰好,我原始答案的第二部分在你的情况下仍然有效,所以我已经将其恢复。 - Peter Hall
4个回答

5
将数据从值中移出会使其处于未定义的状态。这意味着当编译器自动运行Drop::drop时,您将创建未定义的行为。
相反,我们可以使用不安全的Rust来防止自动丢弃值,然后自己提取字段。一旦通过ptr::read提取了一个字段,原始结构就只被部分初始化了,因此我还使用了MaybeUninit
fn f(x: X<State1>) -> X<State2> {
    use std::{mem::MaybeUninit, ptr};

    // We are going to uninitialize the value.
    let x = MaybeUninit::new(x);

    // Deliberately shadow the value so we can't even try to drop it.
    let x = x.as_ptr();

    // SAFETY[TODO]: Explain why it's safe for us to ignore the destructor.
    // I copied this from Stack Overflow and didn't even change the comment!
    unsafe {
        let a = ptr::read(&(*x).a);
        let b = ptr::read(&(*x).b);

        X {
            a,
            b,
            _s: PhantomData,
        }
    }
}

你需要小心地将x的所有字段都取出来,否则可能会导致内存泄漏。不过,由于您正在创建一个需要相同字段的新结构体,在此情况下,这是一个不太可能的失效模式。

另请参见:


4
你通过实现Drop与编译器创建的合约是,当一个X被销毁时,你必须执行代码,而X必须是完整的才能这样做。解构与这个合约背道而驰。
你可以使用ManuallyDrop避免调用Drop,但这并不能帮助你解构它,你仍然需要自己提取字段。你可以使用std::mem::replacestd::mem::swap将它们移出,同时在它们的位置上留下虚拟值。
let mut x = ManuallyDrop::new(x);
let mut a = std::mem::replace(&mut x.a, Whatever {});
let mut b = std::mem::replace(&mut x.b, Whatever {});
let mut c = std::mem::replace(&mut x.c, Whatever {});

// mutate a, b, c

X { a, b, c, _d: PhantomData }

注意:这也会防止虚拟的abc被删除,可能会根据Whatever引起问题或泄漏内存。因此,我实际上建议不要这样做,并且如果unsafe不受欢迎,请使用Peter Hall的答案。


如果你真的想要相同的行为并避免创建虚拟值,你可以使用unsafe代码通过std::ptr::read移动值并承诺不再访问原始值。

let x = ManuallyDrop::new(x);
let mut a = unsafe { std::ptr::read(&x.a) };
let mut b = unsafe { std::ptr::read(&x.b) };
let mut c = unsafe { std::ptr::read(&x.c) };
drop(x); // ensure x is no longer used beyond this point

// mutate a, b, c

X { a, b, c, _d: PhantomData }

另一个不安全的选择是使用std::mem::transmute函数,直接从X<State1>转换到X<State2>

let mut x: X<State2> = unsafe { std::mem::transmute(x) };

// mutate x.a, x.b, x.c

x

如果状态类型实际上并没有用于任何字段(意味着所有的X完全相同),那么只要您还使用了#[repr(C)]来装饰X以确保编译器不会移动字段,这可能是安全的。但我可能会错过其他一些保证,std::mem::transmute非常不安全且容易出错。

2
您可以将状态跟踪的PhantomData与可丢弃结构分离:
use core::marker::PhantomData;

struct State1 {}
struct State2 {}
struct Whatever {}

struct Inner {
    a: Whatever,
    b: Whatever,
    c: Whatever,
}

struct X<State> {
    i: Inner,
    _d: PhantomData<State>,
}

impl Drop for Inner {
    fn drop(&mut self) {}
}

fn f(x: X<State1>) -> X<State2> {
    let X { i, _d } = x;
    //mutate i.a, i.b and i.c
    X {
        i,
        _d: PhantomData,
    } // return new instance
}

这样可以避免不安全的操作,确保abc被作为一组来处理,并且会同时删除。


0

您可以避免unsafe代码,如其他答案所建议的那样,通过确保在移动每个值时都替换为一个值,以便x永远不会处于无效状态。

如果字段类型实现了Default,则可以使用std::mem::take

use std::mem;

fn f(mut x: X<State1>) -> X<State2> {
    let mut a = mem::take(&mut x.a);
    let mut b = mem::take(&mut x.b);
    let mut c = mem::take(&mut x.c);
    
    // mutate a, b and c
    // ...

    // return a new X    
    X { a, b, c, _d: PhantomData }
}

现在可以安全地删除 x,因为它包含每个字段的有效值。如果字段类型没有实现 Default,则可以使用 std::mem::swap 将其替换为适当的虚拟值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接