如果我从未将MutexGuard分配给变量,它在哪里?

15

我不明白内部代码块中的MutexGuard所在的“位置”在哪里。 互斥锁被锁定并解封,生成一个MutexGuard。 不知何故,此代码设法取消引用该MutexGuard,然后可变地借用该对象。MutexGuard去哪了?另外,令人困惑的是,这个取消引用不能替换为deref_mut。为什么?

use std::sync::Mutex;

fn main() {
    let x = Mutex::new(Vec::new());
    {
        let y: &mut Vec<_> = &mut *x.lock().unwrap();
        y.push(3);
        println!("{:?}, {:?}", x, y);
    }

    let z = &mut *x.lock().unwrap();
    println!("{:?}, {:?}", x, z);
}
2个回答

28

简介:因为*x.lock().unwrap()对操作数x.lock().unwrap()进行了隐式借用,所以该操作数被视为是一个位置上下文。但是,由于我们实际的操作数不是一个位置表达式,而是一个值表达式,它被赋给了一个未命名的内存位置(基本上是一个隐藏的let绑定)!

请参见下面的更详细的解释。


位置表达式和值表达式

在我们深入研究之前,首先了解两个重要的术语。在Rust中,表达式分为两大类:位置表达式和值表达式。

  • 位置表达式表示一个有家(内存位置)的值。例如,如果你有let x = 3;那么x就是一个位置表达式。历史上,这被称为左值表达式
  • 值表达式表示一个没有家的值(我们只能使用该值,它没有与之关联的内存位置)。例如,如果你有fn bar() -> i32,那么bar()就是一个值表达式。像3.14"hi"这样的文字也是值表达式。历史上,这些被称为右值表达式

有一个很好的经验法则来检查某个东西是位置还是值表达式:“它是否有意义将其写在赋值符号的左边?”如果有(比如my_variable = ...;),那么它就是一个位置表达式;如果没有(比如3 = ...;),那么它就是一个值表达式。

还有位置上下文值上下文。这些基本上是表达式可以放置的“插槽”。只有很少的位置上下文,它们(通常情况下,请见下文)需要一个位置表达式

  • 复合赋值表达式的左侧 (⟨place context⟩ = ...;, ⟨place context⟩ += ...;)
  • borrow 表达式的操作数 (&⟨place context⟩&mut ⟨place context⟩)
  • ... 还有更多

请注意,place 表达式比较 "强大"。它们可以在值上下文中使用而不会出现问题,因为它们还表示一个值。

(参考手册中相关章节)

临时生命周期

让我们构建一个小的虚拟示例来演示 Rust 的一件事情:

struct Foo(i32);

fn get_foo() -> Foo {
    Foo(0)
}

let x: &Foo = &get_foo();

这很有效!

我们知道表达式get_foo()是一个值表达式。我们也知道借用表达式的操作数是一个位置上下文。那么为什么这个代码可以编译?难道不需要位置表达式吗?

Rust会创建临时的let绑定!来自the reference

在大多数位置表达式上下文中使用值表达式时,将创建一个未命名的临时内存位置,该位置初始化为该值,并将表达式评估为该位置[...]。

因此,上面的代码等同于:

let _compiler_generated = get_foo();
let x: &Foo = &_compiler_generated;

这就是使你的Mutex示例工作的原因:将MutexLock分配给一个临时的无名内存位置!它就住在那里。让我们看一下:

&mut *x.lock().unwrap();
x.lock().unwrap()部分是一个值表达式:它具有类型MutexLock,并像上面的get_foo()一样由函数(unwrap())返回。然后只剩下一个问题:解引用运算符*的操作数是否为place context?我没有在上面的place contests列表中提到它...

隐式借用

拼图中的最后一块是隐式借用。来自the reference:

某些表达式将通过隐式借用将表达式视为place expression。

这些包括"解引用运算符(*)的操作数"!任何隐式借用的所有操作数都是place contexts!

因此,因为*x.lock().unwrap()执行了隐式借用,操作数x.lock().unwrap()是一个place context,但由于我们的实际操作数不是一个place,而是一个值表达式,所以它被赋值给一个未命名的内存位置!

为什么对于deref_mut()不起作用

有一个重要的细节是"temporary lifetimes"。让我们再看一下引用:

在大多数place expression上下文中使用值表达式时,将创建一个临时的未命名内存位置,并将其初始化为该值,表达式将计算为该位置[...]。

根据情况,Rust会选择不同生命周期的内存位置!在上面的&get_foo()示例中,临时未命名的内存位置具有封闭块的生命周期。这相当于我上面展示的隐藏let绑定。
然而,“临时未命名的内存位置”并不总是等同于let绑定!让我们看看这种情况:
fn takes_foo_ref(_: &Foo) {}

takes_foo_ref(&get_foo());

在这里,Foo值仅存在于takes_foo_ref调用的持续时间内,不能再长时间存在!
通常,如果对临时对象的引用用作函数调用的参数,则该临时对象仅存在于该函数调用中。这也包括&self(和&mut self)参数。因此,在get_foo().deref_mut()中,Foo对象也只会在deref_mut()的持续时间内存在。但由于deref_mut()返回对Foo对象的引用,因此我们将得到“ does not live long enough”错误。
当然,对于x.lock().unwrap().deref_mut()也是如此 - 这就是为什么我们会收到错误消息的原因。
在解引用操作符(*)的情况下,临时对象存在于封闭块(等效于let绑定)中。我只能假设这是编译器中的特殊情况:编译器知道对deref()deref_mut()的调用总是返回对self接收器的引用,因此仅将临时对象借用给函数调用没有意义。

但是在这里,临时变量的生命周期似乎是封闭块而不是最内层的封闭语句? - CodesInChaos
@CodesInChaos 谢谢你发现了这个问题!我当时很匆忙,忽略了这个重要的细节。我已经修正了我的回答,现在(几乎)一切都讲得通了! - Lukas Kalbertodt

8
这里是我的想法:
let y: &mut Vec<_> = &mut *x.lock().unwrap();

对于您当前的代码,下面有几个需要注意的地方:

  1. .lock() 返回一个 LockResult<MutexGuard<Vec>>
  2. 您在 LockResult 上调用了 unwrap(),获得了一个 MutexGuard<Vec>
  3. 因为 MutexGuard<T> 实现了 DerefMut 接口,Rust 执行了解引用强制转换。它被 * 运算符解引用,并返回一个 &mut Vec

在 Rust 中,我认为您不需要自己调用 deref_mut,编译器会为您执行 Deref coercion

如果您想要获取您的 MutexGuard,则不应该对其进行解引用操作:

let mut y  = x.lock().unwrap();
(*y).push(3);
println!("{:?}, {:?}", x, y);
//Output: Mutex { data: <locked> }, MutexGuard { lock: Mutex { data: <locked> } }

从我在网上看到的情况来看,人们通常会将 MutexGuard 显式地保存到一个变量中,并在使用时对其进行解引用,就像上面修改后的代码一样。我认为没有官方的模式来处理这个问题。有时这也可以避免您创建临时变量。


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