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