有没有方法让不可变引用变成可变的?

15

我希望使用 Rust 解决一道力扣问题 (删除链表的倒数第 N 个节点)。我的解法使用两个指针来找到需要移除的 Node

#[derive(PartialEq, Eq, Debug)]
pub struct ListNode {
    pub val: i32,
    pub next: Option<Box<ListNode>>,
}

impl ListNode {
    #[inline]
    fn new(val: i32) -> Self {
        ListNode { next: None, val }
    }
}

// two-pointer sliding window
impl Solution {
    pub fn remove_nth_from_end(head: Option<Box<ListNode>>, n: i32) -> Option<Box<ListNode>> {
        let mut dummy_head = Some(Box::new(ListNode { val: 0, next: head }));
        let mut start = dummy_head.as_ref();
        let mut end = dummy_head.as_ref();
        for _ in 0..n {
            end = end.unwrap().next.as_ref();
        }
        while end.as_ref().unwrap().next.is_some() {
            end = end.unwrap().next.as_ref();
            start = start.unwrap().next.as_ref();
        }
        // TODO: fix the borrow problem
        // ERROR!
        // start.unwrap().next = start.unwrap().next.unwrap().next.take();
        dummy_head.unwrap().next
    }
}

我借用了链表的两个不可变引用。在我找到要删除的目标节点后,我想放弃其中一个引用并将另一个引用改为可变。以下每个代码示例都会导致编译器错误:

// ERROR
drop(end); 
let next = start.as_mut().unwrap.next.take();

// ERROR
let mut node = *start.unwrap()

我不知道这个解决方案是否可以用Rust编写。如果我想将一个不可变引用变为可变的,我该怎么做?如果不行,有没有其他方式实现相同的逻辑,同时让借用检查器满意呢?


6
将不可变引用转换为可变引用几乎从来都不是一个好主意。你应该一开始就借用为可变的。 - Bartek Banachewicz
3
否则,在您的数据结构中使用内部可变性,例如RefCell - Peter Hall
3
您可能希望查看《使用太多链表学习 Rust》(Learning Rust with entirely too many linked lists)。链接为https://cglab.ca/~abeinges/blah/too-many-lists/book/。 - Jmb
4
我认为这些负评并不合理。是的,你不能在没有未定义行为的情况下完成这个操作,但这并不是一个不合理的问题 -- 特别是对于那些来自像C++这样的语言,其中“const”更多的是一种“建议”而不是一条“规则”的用户来说。 - trent
7
“我能开枪朝自己头部射击吗?” - Jim Mischel
显示剩余5条评论
3个回答

16

正确的答案是你不应该这样做。这是未定义行为,会破坏编译器在编译程序时所做的许多假设。

但是,有可能做到这一点。其他人也提到了为什么这不是一个好主意,但他们实际上没有展示出如何编写类似这样的代码。即使你不应该这样做,这就是它的样子:

unsafe fn very_bad_function<T>(reference: &T) -> &mut T {
    let const_ptr = reference as *const T;
    let mut_ptr = const_ptr as *mut T;
    &mut *mut_ptr
}

本质上,您将一个常量指针转换为可变指针,然后再将可变指针转换为引用。

以下是为什么这样做非常不安全和不可预测的一个例子:

fn main() {
    static THIS_IS_IMMUTABLE: i32 = 0;
    unsafe {
        let mut bad_reference = very_bad_function(&THIS_IS_IMMUTABLE);
        *bad_reference = 5;
    }
}

如果你运行这段代码,你会得到一个段错误。发生了什么?实际上,你试图写入被标记为不可变的内存区域,从而使内存规则无效。当你使用这样的函数时,你破坏了编译器与你之间的信任,不要去干扰常量内存。
这就是为什么你永远不应该在公共API中使用这个函数,因为如果有人向你的函数传递一个无辜的不可变引用,并且你的函数对其进行了改变,而引用指向的是不允许写入的内存区域,你将会得到一个段错误。
简而言之:不要试图欺骗借用检查器,它存在是有原因的。
编辑:除了我刚刚提到的原因外,另一个原因是打破引用别名规则。也就是说,由于你可以同时拥有一个可变和不可变的变量引用,所以当你将它们分别传递给同一个函数时,会导致很多问题。该函数假设不可变和可变引用是唯一的。阅读Rust文档中的此页面获取更多信息。

2
这个回答是误导性的。在Rust中,与C/C ++不同,仅仅将&引用转换为&mut引用是不安全的,即使底层对象可以被修改。这是因为&mut引用就像C中的restrict限定指针一样,将T *强制转换为T * restrict可能会导致未定义的行为,因为编译器可以基于restrict假设进行优化加载和存储操作。 - trent
1
换句话说,这个问题的答案在Rust中不适用。你不仅要保证引用是可变的,还必须保证&T没有别名。 - trent
3
这个函数是“未定义行为”,没有任何一种使用方式是安全的。尝试用Miri运行它。我没有提供永远不能安全使用的代码,这是有原因的。 - Shepmaster
4
无法展示某个行为属于未定义行为。根据定义,未定义行为意味着任何事情都有可能发生。运行该代码可能会擦除您的硬盘或召唤鼻子恶魔。它碰巧崩溃的事实基本上是幸运的。 - Shepmaster
3
假设“如果你运行这个程序,你会得到一个segfault”,表明编译器实际上绑定在将代码编译为某些有意义的内容,但在运行时提供的值下是错误的。实际上,“very_bad_function”可能根本就没有被编译成有意义的东西,因为优化器可能会基于&mut T的非别名性做出可达性的假设。违反别名规则不是“额外”的原因,“除了”写入只读内存的问题 - 这是这段代码具有未定义行为的唯一*原因... - trent
显示剩余2条评论

12

10
《Nomicon》有一段相关的内容,我非常欣赏:“将 & 转变为 &mut 是未定义行为;无论如何将 & 转变为 &mut 都是未定义行为;不可以这样做;你并没有特殊之处”。 - E net4

1
尽管被接受的答案确实为OP的特定问题提供了更安全的实现参考,但并不完全正确地说,在不引发未定义行为的情况下无法更改不可变引用中的数据。
如果不可能的话,Rust就无法实现Mutex。因此,我们必须假设存在一种无UB的方法来实现它,这在Mutex之外也很有用,例如,当您要实现原子同步以追求额外速度时。
我看到到处都存在混淆的根源可能是以下概念的混合:
1. 编译器是否生成了正确的汇编代码? 2. 根据Rust的安全前提,我的程序是否仍然正确?
当然,要将&转换为&mut,您需要使用不安全的代码——在这种情况下,Rust相信您更了解,因此自动化的(2)保证将在不安全区域关闭。
当升级不可变引用时,未定义行为出现在(1)中,当编译器进行假设数据是只读的优化时(例如将其存储到一组寄存器中)。
话虽如此,我将展示两种方法来完成它:第一种是通过不提示编译器引入一个未定义行为(UB),第二种是按照最新的 Rust 夜间编译器(1.72)建议的正确方式来完成。
// DON'T DO IT! UNDEFINED BEHAVIOR -- it compiles in Rust stable 1.70, but might produce the wrong assembly
let ub_mutable_self = unsafe { &mut *((ref_self as *const Self) as *mut Self) };

// OK CODE: The Compiler Intrinsics `UnsafeCell` tells LLVM that read-only optimizations should not be used
struct MyStruct {
    data: UnsafeCell<u32>,
}

impl MyStruct {
    
    fn inc(&self) {
        let mutable_data = unsafe {&mut * self.data.get()};
        *mutable_data += 1;
    }
}

在操场上看看它(并尝试Miri分析)

最后一点,我建议Rust的新手不要使用这种技术:绕过了Rust编译器的检查可能很诱人,但这些检查是为了告诉你的程序何时出错。总是有其他选择(在语言和std::中),让你重新思考设计并编写优雅且正确的代码,而不使用unsafe


这仍然是未定义行为(UB)。尽管我不确定,但你将指针转换为UnsafeCell可能会使编译器无法对其进行优化,但根据Rust规则,它仍然属于未定义行为。 - Chayim Friedman
并且Miri也检测到 - Chayim Friedman
就像《Nomicon》(E_net4引用的)所说:“将&转换为&mut始终是未定义行为。不,你不能这样做。不,你并没有特殊待遇。” - Chayim Friedman
感谢大家的意见。根据你们的建议,我提供了一个经过审查的版本,并对UnsafeCell的使用进行了修正,现在它按预期工作了--https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=788d2daa9b3faf4adafbe2c0883187bf - zertyz
现在没问题。 - Chayim Friedman
显示剩余5条评论

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