【问题标题】:Temporarily move out of borrowed content暂时搬出借来的内容
【发布时间】:2015-04-10 21:12:56
【问题描述】:

我正在尝试替换可变借用中的值;将其中的一部分移入新值:

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 会破坏它的完整性。但由于该值在之后立即被删除,我(如果不是编译器)可以保证它是安全的。

有什么方法可以实现吗?我觉得这是不安全代码的工作,但我不确定它会如何工作。

【问题讨论】:

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

标签: rust ownership borrow-checker


【解决方案1】:

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

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

另见:

【讨论】:

    【解决方案2】:

    上面的代码不起作用,可以理解的是,移动值 出于自我破坏了它的完整性。

    这并不是这里发生的事情。例如,与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。这些函数允许你从可变引用中“获取”一个值,前提是你提供一些东西作为交换。

    【讨论】:

    • 当可以只使用&amp;mut 来实现它时,要求方法的调用者拥有Foo 对我来说毫无意义。在这种情况下不需要所有权,因为可以保证self 的完整性。
    • @azgult 这正是很多人请求&amp;own-like 指针的原因(请参阅我提供的 RFC 和问题的链接)——因为事实上这样的事情 做 i> 需要所有权(只有所有者可以移动值)。
    【解决方案3】:

    好的,我想出了一点unsafeness 和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() {}
    

    【讨论】:

    • 如果T 有析构函数,这个程序将严重失败。当您致电swap 时,您正在用垃圾替换位于self 的任何内容。然后你重新分配*self,Rust 将插入一个对析构函数的调用,它会尝试破坏*self 的“旧”值,它现在是垃圾。出于某种原因,playpen 没有失败(但你可以在那里看到 double free),但对我来说,当我在本地编译和运行它时,程序核心会转储。
    • This 程序更清楚地演示了何时以及如何调用析构函数。如果你的程序是安全的,它只会被调用一次,但它会被调用两次——第一次是错误的。
    • 好收获。我相信使用std::ptr::write 的修改版本应该是安全的。
    • 不,这不安全,现在您实际上正在丢弃未初始化的内存。您需要在write 调用后将mem::forget tmp 变量调用
    • take 展示了如何以通用方式执行此操作,并且还可以防止展开(而不是中止)。
    猜你喜欢
    • 2023-03-11
    • 1970-01-01
    • 2016-09-15
    • 2018-04-04
    • 2019-08-29
    • 2015-04-20
    • 1970-01-01
    • 1970-01-01
    • 2019-10-18
    相关资源
    最近更新 更多