可变引用是否具有移动语义?

36
fn main() {
    let mut name = String::from("Charlie");
    let x = &mut name;
    let y = x;       // x has been moved
    say_hello(y);
    say_hello(y);       // but y has not been moved, it is still usable
    change_string(y);
    change_string(y);  

}

fn say_hello(s: &str) {
    println!("Hello {}", s);
}

fn change_string(s: &mut String) {
    s.push_str(" Brown");
}

当我将x分配给y时,x已经移动了。然而,我期望使用移动语义的内容在函数中使用时也会被移动。但是,在后续调用之后仍然可以使用该引用。也许这与say_hello()接受不可变引用,而change_string()接受可变引用有关,但该引用仍未被移动。

1个回答

55
你的推理和观察完全正确。事实上,情况看起来确实应该是你描述的那样发生的。然而,编译器在这里应用了一些方便的魔法。
在Rust中,移动语义通常适用于所有未实现Copy trait的类型。共享引用是可复制的,因此当被赋值或传递给函数时,它们只是被复制。可变引用不可复制,所以它们应该被移动。
这就是魔法开始的地方。每当一个可变引用被赋值给一个已经被编译器知道是可变引用类型的名称时,原始引用会被隐式重新借用,而不是被移动。因此,函数调用
change_string(y);

由编译器转换为的含义。
change_string(&mut *y);

原始引用被解引用,并创建一个新的可变借用。这个新的借用被移入函数中,在函数返回后原始借用被释放。
请注意,这不是函数调用和赋值之间的区别。隐式重新借用发生在编译器已知目标类型为可变引用的情况下,例如因为模式具有显式类型注释。因此,由于我们明确将其注释为可变引用类型,这一行也创建了一个隐式重新借用。
let y: &mut _ = x;

另一方面,这个函数调用移动(并因此消耗)可变引用y

fn foo<T>(_: T) {}

[...]
foo(y);

通用类型 `T` 在这里并没有明确地表示可变引用类型,所以不会发生隐式重借用,即使编译器推断该类型是可变引用类型 - 就像你的赋值语句 `let y = x;` 的情况一样。
在某些情况下,即使没有显式的类型注解,编译器也可以推断出泛型类型是可变引用类型。
fn bar<T>(_a: T, _b: T) {}

fn main() {
    let mut i = 42;
    let mut j = 43;
    let x = &mut i;
    let y = &mut j;
    bar(x, y);   // Moves x, but reborrows y.
    let _z = x;  // error[E0382]: use of moved value: `x`
    let _t = y;  // Works fine. 
}

当推断第一个参数的类型时,编译器还不知道它是可变引用,因此没有隐式重新借用发生,并且 x 被移动到函数中。然而,当到达第二个参数时,编译器已经推断出 T 是可变引用,因此 y 被隐式重新借用。(这个例子很好地说明了为什么在编译器中添加魔法以使事情“正常工作”通常是一个坏主意。显式比隐式更好。)

不幸的是,目前Rust参考文档中还没有记录此行为。

另请参阅:


重新借用的好处是什么?它能提高性能吗?它能防止某些类型的错误吗? - Shiva
2
@Shiva 另一种选择是移动可变引用,这意味着它不能再次使用。性能不会受到任何影响 - 生成的代码应该基本相同。区别在于借用检查器如何解释它。 - Sven Marnach

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