首页 > 解决方案 > 暂时放弃 Rust 中可变借用内容的所有权是否安全?

问题描述

一个通过函数修改&mut T原位的函数FnOnce(T) -> T是否可以安全地生锈,还是会导致未定义的行为?它是包含在某个地方的标准库中,还是包含在一个知名的 crate 中?

如果您还假设T: Default,那看起来像

fn modify<T, F: FnOnce(T) -> T>(x: &mut T, f: F) -> ()
where
    T: Default
{
    let val = std::mem::take(x);
    let val = f(val);
    *x = val;
}

(另见 https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f015812bac6f527fe663fe4e0b7a3188

我的问题是关于做同样的事情但放弃where T: Default条款(也没有T: Clone)。这需要不同的实现,因为您不能使用std::mem::take. 我不确定如何实现不受约束的版本,但应该可以使用不安全的 Rust。

我正在从线性类型和子结构逻辑的背景中学习 Rust。Rust 的可变借用似乎与将资源移入然后移出函数非常相似,但我不知道像这样临时拥有可变借用的内容是否真的安全。

标签: rustborrow-checkerownership

解决方案


它是安全的,甚至还有板条箱(现在找不到)。

然而。

在编写不安全的代码时,您必须非常小心。如果你不知道自己在做什么,很容易导致UB。

例如,这里有一些你可能没有想到的东西:恐慌安全。

假设我们简单地实现它:

pub fn modify<T, F: FnOnce(T) -> T>(v: &mut T, f: F) {
    let prev = unsafe { std::ptr::read(v) };
    let new = f(prev);
    unsafe { std::ptr::write(v, new) };
}

微不足道的对。

或者是吗?

fn main() {
    struct MyStruct(pub i32);
    impl Drop for MyStruct {
        fn drop(&mut self) {
            println!("MyStruct({}) dropped", self.0);
        }
    }

    let mut v = MyStruct(123);
    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        modify(&mut v, |_prev| {
            // `prev` is dropped here.
            panic!("Haha, evil panic!");
        })
    }))
    .unwrap_err();
    v.0 = 456; // Writing to an uninitialized memory!
               // `v` is dropped here, double drop!
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6f7312a8be70cd43cf5cf7a9816be56a

我使用了一个自定义类型,它的析构函数除了打印之外什么都不做,但是想象一下如果这是一个Vec释放内存并且我们正在写入释放的内存(然后,作为奖励,获得双重释放)会发生什么。

就像@Kendas 所说的那样,当没有中断点时,在 Rust 中将内存保持在未初始化状态是有效的,这是正确的。问题是,实际上中断点比您希望的要多得多。事实上,在编写不安全的代码时,您必须考虑对外部代码的任何调用(即不是您的代码,也不是您相信不会做坏事的代码,例如 std)是一个中断点。

不安全的代码很难。最好留在安全的土地上。

编辑:您可能想知道它AssertUnwindSafe是什么。也许您甚至尝试删除它并注意到它没有编译。嗯,UnwindSafe这是一种保护,AssertUnwindSafe是一种绕过保护的方法。

你可能会问,这有什么意义?关键是,这种保护确实不准确。如此不准确,绕过它甚至不需要不安全。但它仍然存在,所以我们意外UB的可能性较低。

作为 API 的编写者,这对你来说并不重要——你应该表现得好像这种保护不存在,因为绕过它是安全的,而且很容易被错误地绕过。Rust 标准库本身在过去也有类似的错误(#86443#81740,... - 它们都在同一个代码中并非偶然 - 这些问题往往以块的形式出现。但是有更多的)。


推荐阅读