有没有办法使不可变引用可变?

Posted

技术标签:

【中文标题】有没有办法使不可变引用可变?【英文标题】:Is there a way to make an immutable reference mutable? 【发布时间】:2019-06-11 18:03:39 【问题描述】:

我想用 Rust (Remove Nth Node From End of List) 解决一个 leetcode 问题。我的解决方案使用两个指针来查找要删除的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 编写。如果我可以使不可变引用可变,我该怎么做?如果没有,是否有在使借用检查器满意的同时实现相同的逻辑?

【问题讨论】:

将不可变引用转换为可变引用几乎不是一个好主意。你应该首先借用可变的。 或者在你的数据结构中使用内部可变性,比如RefCell 你可能想看看Learning Rust with entirely too many linked lists 我认为没有必要投反对票。不,如果没有 UB,你不能这样做,但这不是一个不合理的问题——尤其是对于来自像 C++ 这样的语言的用户来说,constness 实际上更像是一个 suggestion 而不是 规则. 翻译:“有可能朝自己的头部开枪吗?” 【参考方案1】:

有没有办法使不可变引用可变?

没有。

您可以编写不安全的 Rust 代码来强制类型对齐,但​​代码实际上是不安全的并导致未定义的行为。你不想要这个。


对于您的具体问题,请参阅:

How to remove the Nth node from the end of a linked list? How to use two pointers to iterate a linked list in Rust?

【讨论】:

Nomicon 有一段我非常欣赏的相关段落:"将 & 转换为 &mut 是 UB;将 & 转换为 &mut 始终是 UB;不,你不能这样做; 不,你并不特别”【参考方案2】:

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

但是,可以这样做。其他人也提到了为什么这不是一个好主意,但他们实际上并没有展示做这样的事情的代码是什么样的。即使您不应该这样做,它也应该是这样的:

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 文档中的 this page 了解更多信息。

【讨论】:

这个答案具有误导性。在 Rust 中,与 C/C++ 不同,将 &amp; 引用转换为 &amp;mut 引用是不合理的,即使底层对象可能会发生变异。这是因为 &amp;mut 引用就像 C 中的 restrict 限定指针,并且将 T* 强制转换为 T* restrict 会导致未定义的行为,因为允许编译器基于 restrict 优化加载和存储假设。 换句话说,this question 的答案不适用于 Rust。除了保证所指对象是可变的之外,您还必须保证 &amp;Tunaliased 这个函数是未定义的行为没有种可​​能的安全使用方式。 Try running it with Miri。我的回答没有提供永远无法安全使用的代码是有原因的。 说明为什么会出现这种未定义的行为——你不能证明某事是未定义的行为。根据定义,UB 意味着任何事情都可能发生。运行该代码可以擦除您的硬盘驱动器或召唤鼻恶魔。发生段错误的事实基本上是运气。 假设“如果你运行这个......你得到一个段错误”表明编译器实际上必须将代码编译成有意义的东西,但考虑到运行时提供的值是不正确的。事实上,very_bad_function 可能根本不会被编译成任何有意义的东西,因为优化器可能会基于&amp;mut T 的非别名来假设可达性。打破别名规则不是“除了”写入只读内存的问题之外的额外原因——这是此代码具有未定义行为的唯一原因。 .

以上是关于有没有办法使不可变引用可变?的主要内容,如果未能解决你的问题,请参考以下文章

对String值不可变的理解以及String类型的引用传递问题

如何使 Rust 可变引用不可变?

python中的引用传递,可变对象,不可变对象,list注意点

Python的可变类型与不可变类型

Rust 编译器不期望一个可变引用,而应该期望一个可变引用

python 函数传递可变不可变对象