为什么可变引用在这里没有被移动?

25
我之前认为可变引用(即&mut T)总是会被移动。这很合理,因为它们允许独占的可变访问。 在下面的代码中,我将一个可变引用赋值给另一个可变引用,原始引用被移动了。因此,我不能再使用原始引用了:
let mut value = 900;
let r_original = &mut value;
let r_new = r_original;
*r_original; // error: use of moved value *r_original

如果我有这样一个函数:
fn make_move(_: &mut i32) {
}

并修改我的原始示例,使其看起来像这样:
let mut value = 900;
let r_original = &mut value;
make_move(r_original);
*r_original; // no complain

我原本以为当我使用函数make_move并传入可变引用r_original时,该引用会被移动。但实际上并未发生。在调用后我仍然能够使用该引用。
如果我使用通用函数make_move_gen
fn make_move_gen<T>(_: T) {
}

并像这样调用:

let mut value = 900;
let r_original = &mut value;
make_move_gen(r_original);
*r_original; // error: use of moved value *r_original

引用被移动,因此程序的行为符合我的预期。 调用函数make_move时,为什么引用没有被移动?

代码示例


2
显式实例化(make_move::<&mut i32>(r_original);)的工作方式类似于原始函数(没有移动)。有趣的是,我会认为在类型推断之前进行借用检查。 - Veedrac
从 dacker 的回答中,我认为是这种情况:明确注释类型为可变引用会触发内容的重新借用,而不是移动,一旦新的引用(在 make_move 的作用域中)超出范围,原始引用就可以再次使用。 - jtepe
4个回答

16

这背后可能有一个很好的原因。

&mut T 实际上并不是一种类型:所有借用都由一些(可能无法表达)生命周期参数化。

当我们写下:

fn move_try(val: &mut ()) {
    { let new = val; }
    *val
}

fn main() {
    move_try(&mut ());
}

类型推断引擎推断typeof new == typeof val,因此它们共享原始生命周期。这意味着从new借用的不会结束,直到从val借用结束。

这意味着它等同于

fn move_try<'a>(val: &'a mut ()) {
    { let new: &'a mut _ = val; }
    *val
}

fn main() {
    move_try(&mut ());
}

然而,当你撰写时,需要注意以下几点。
fn move_try(val: &mut ()) {
    { let new: &mut _ = val; }
    *val
}

fn main() {
    move_try(&mut ());
}

发生了一个强制转换 - 这与允许你取消指针可变性的相同类型的事情有关。这意味着生命周期是一些(似乎无法规定的)'b < 'a。这涉及到一个强制转换,因此要重新借用,所以重新借用就能够超出范围。

一个始终重新借用的规则可能会更好,但明确声明也不是问题。


我从未这样想过。不过这似乎很合理。你所说的“放弃指针可变性”是指从可变引用获取共享引用吗?比如 let r: &i32 = r_mut; 其中 r_mut 是 &mut i32。 - jtepe
很好的解释。这真的很有道理。关于 'b 我的猜测是:它是 'a 的交集,包括 move_try 主体和定义 new 的嵌套作用域;所以 'b 将是这个嵌套作用域,在其中 new 借用结束的最后一次。 - dacker
根据解释,前两个代码片段似乎是等价的,但它们显示不同的错误消息Playground。第一个片段说值已经移动了,而第二个片段则说仍在借用中。我有误解吗? - Mihir Luthra
1
@Mihir 从技术上讲,let new:&'a mut _ = val; 是一个重新借用,只是因为生命周期相同而无操作。 - Veedrac
@Veedrac,你说“&mut T其实并不是一种类型”是有道理的。我很好奇,“let new = val”会移动可变引用。那么,如果我们必须明确注释"new"想要将数据从"val"移动到"new",我们该怎么做? let new: _ = val。"_"的确切类型是什么? - Mihir Luthra
1
@Mihir 我认为你不能这样做,除非你将整个类型存储在类型变量中。 - Veedrac

5

我曾在这里询问过类似的问题。

看起来在某些(许多?)情况下,重新借用取代了移动。内存安全性没有被破坏,只是“已移动”的值仍然存在。我也找不到有关该行为的任何文档。

@Levans在这里开了一个GitHub issue,尽管我并不完全相信这只是一个文档问题:可靠地从&mut引用中移出似乎是Rust所有权方法的核心。


4

1
如果我稍微调整一下通用的那个,它也不会抱怨。
fn make_move_gen<T>(_: &mut T) {
}

或者

let _ = *r_original; 

这并没有真正帮助回答这种行为背后的“为什么”。而第二个例子只是解除引用,并不真正相关。 - kmdreko
@kmdreko 第二个例子中,我指的是如果我将问题代码块中的“*r_original;”更改为“let _ = *r_original;”,它就能编译通过。 我正在尝试一些东西,我发现当我执行“let _ = r_original;”时,它不会进行解引用,因此即使该值已经移动,它也不会发出投诉。 - ggaowp
哦,那很有趣!它确实解决了错误。对于我的误解,我改变了我的投票。 - kmdreko
谢谢你的评论,我长期以来一直是一个纯消费者,现在学习回馈一点贡献 :) - ggaowp

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