暂时移出借用的内容

13

我正在尝试替换可变借用中的一个值;将部分内容移动到新值中:

enum Foo<T> {
    Bar(T),
    Baz(T),
}

impl<T> Foo<T> {
    fn switch(&mut self) {
        *self = match self {
            &mut Foo::Bar(val) => Foo::Baz(val),
            &mut Foo::Baz(val) => Foo::Bar(val),
        }
    }
}

上面的代码无法工作,可以理解为把值从self中移出会破坏其完整性。但由于该值立即被丢弃,因此我(而不是编译器)可以保证其安全性。

有没有办法实现这一点?我觉得这是一个不安全代码的工作,但我不确定如何使用。


1
如果您添加一个绑定到TCopy,您的代码实际上可以工作,尽管我显然不知道您是否接受这种限制。 - fjh
3个回答

10

mem:uninitialized已经在Rust 1.39版本中被弃用,取而代之的是MaybeUninit

然而,在这里不需要未初始化的数据。相反,您可以使用ptr::read获取由self引用的数据。

此时,tmp拥有枚举中的数据,但是如果我们放弃self,析构函数将尝试读取该数据,从而导致内存不安全。

然后我们执行转换并将值放回,恢复类型的安全性。

use std::ptr;

enum Foo<T> {
    Bar(T),
    Baz(T),
}

impl<T> Foo<T> {
    fn switch(&mut self) {
        // I copied this code from Stack Overflow without reading
        // the surrounding text that explains why this is safe.
        unsafe {
            let tmp = ptr::read(self);
    
            // Must not panic before we get to `ptr::write`

            let new = match tmp {
                Foo::Bar(val) => Foo::Baz(val),
                Foo::Baz(val) => Foo::Bar(val),
            };
    
            ptr::write(self, new);
        }
    }
}

更高级的代码版本将防止恐慌从此代码中冒出,并导致程序中止。

另请参见:


7
上面的代码不起作用,这是可以理解的,因为移动值会破坏self的完整性。
这里并不完全是这种情况。例如,对于self相同的事情可以很好地工作:
impl<T> Foo<T> {
    fn switch(self) {
        self = match self {
            Foo::Bar(val) => Foo::Baz(val),
            Foo::Baz(val) => Foo::Bar(val),
        }
    }
}

Rust对于部分和全部移动都没有问题。问题在于,您并不拥有您正在尝试移动的值 - 您只有一个可变借用引用。您不能从任何引用(包括可变引用)中移出。这实际上是经常请求的功能之一 - 一种特殊类型的引用,允许从中移出。它将允许几种有用的模式。您可以在此处找到更多信息:herehere
与此同时,对于某些情况,您可以使用std::mem::replacestd::mem::swap。这些函数允许您从可变引用中“取出”一个值,前提是您提供了交换物。

如果只需要使用 &mut 就能实现,那么要求方法的调用者拥有 Foo 是没有意义的。在这种情况下不应该要求所有权,因为可以保证 self 的完整性。 - azgult
@azgult 这正是为什么很多人请求类似于 &own 的指针(请参见我提供的 RFC 和问题链接)的原因 - 因为这样的东西实际上确实需要所有权(只有所有者才能移动值)。 - Vladimir Matveev

4

好的,我通过使用一些不安全的代码和 std::mem 来解决了这个问题。

我用一个未初始化的临时值替换了 self。由于现在我“拥有”了原来的 self,所以我可以安全地将其值移出并替换它:

use std::mem;

enum Foo<T> {
    Bar(T),
    Baz(T),
}

impl<T> Foo<T> {
    fn switch(&mut self) {
        // This is safe since we will overwrite it without ever reading it.
        let tmp = mem::replace(self, unsafe { mem::uninitialized() });
        // We absolutely must **never** panic while the uninitialized value is around!

        let new = match tmp {
            Foo::Bar(val) => Foo::Baz(val),
            Foo::Baz(val) => Foo::Bar(val),
        };

        let uninitialized = mem::replace(self, new);
        mem::forget(uninitialized);
    }
}

fn main() {}

4
如果 T 有析构函数,那么这个程序将会失败。当你调用 swap 函数时,你会用垃圾替换掉 self 所在的位置。然后你会重新给 *self 赋值,Rust 将会插入一个销毁函数的调用,尝试去销毁 *self 的“旧”值,这个“旧”值此时就是垃圾了。出于某种原因 playpen 没有失败(但你可以看到 double free),但是当我在本地编译和运行它时,这个程序会核心转储。 - Vladimir Matveev
1
这个程序更清楚地演示了析构函数何时以及如何被调用。如果你的程序是安全的,它只会被调用一次,但实际上它被调用了两次——第一次是错误的。 - Vladimir Matveev
不错的发现。我相信使用 std::ptr::write 的修改版本应该是安全的。 - azgult
1
不,这样并不安全,现在你实际上正在释放未初始化的内存。在write调用之后,你需要使用mem::forget函数来处理tmp变量。 - oli_obk
2
take 展示了如何以一种通用的方式实现此操作,并且还可以防止出现异常情况(而是中止)。 - Stefan
显示剩余2条评论

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