Где MutexGuard, если я никогда не назначаю его переменной?

Я не понимаю, «где» находится 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);
}

person AnimatedRNG    schedule 14.07.2018    source источник


Ответы (2)


Вывод: поскольку *x.lock().unwrap() выполняет неявное заимствование операнда x.lock().unwrap() операнд рассматривается как контекст места. Но поскольку наш реальный операнд — это не выражение места, а выражение значения, он присваивается безымянной ячейке памяти (фактически скрытая привязка let)!

Более подробное объяснение см. ниже.


Место выражения и выражение значения

Прежде чем мы углубимся, первые два важных термина. Выражения в Rust делятся на две основные категории: выражения места и выражения значения.

  • Выражения места представляют значение, у которого есть дом (ячейка памяти). Например, если у вас есть let x = 3;, то x — это выражение места. Исторически это называлось выражение lvalue.
  • Выражения-значения представляют собой значение, не имеющее дома (мы можем использовать только значение, с ним не связано место в памяти). Например, если у вас есть fn bar() -> i32, то bar() является выражением значения. Литералы вроде 3.14 или "hi" также являются выражениями-значениями. Исторически они назывались выражениями rvalue.

Существует хорошее эмпирическое правило для проверки того, является ли что-то выражением места или значения: «имеет ли смысл писать это в левой части присваивания?». Если да (например, my_variable = ...;), то это выражение места, если нет (например, 3 = ...;), то это выражение значения.

Также существуют контексты мест и контексты значений. Это в основном «слоты», в которые могут быть помещены выражения. Есть только несколько контекстов места, которые (обычно см. ниже) требуют выражения места:

  • Левая часть (составного) выражения присваивания (⟨place context⟩ = ...;, ⟨place context⟩ += ...;)
  • Операнд выражения заимствования (&⟨place context⟩ и &mut ⟨place context⟩)
  • ... плюс еще несколько

Обратите внимание, что выражения места строго более «мощны». Их можно без проблем использовать в контексте значения, поскольку они также представляют значение.

(соответствующая глава справочника)

Временные жизни

Давайте создадим небольшой фиктивный пример, чтобы продемонстрировать, что делает Rust:

struct Foo(i32);

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

let x: &Foo = &get_foo();

Это работает!

Мы знаем, что выражение get_foo() является выражением-значением. И мы знаем, что операндом выражения заимствования является контекст места. Итак, почему это компилируется? Разве контексты размещения не нуждались в выражениях размещения?

Rust создает временные привязки let! Из ссылки:

При использовании выражения значения в большинстве контекстов выражения места создается временная неименованная ячейка памяти, инициализированная этим значением, и вместо этого выражение вычисляется по этой ячейке [...].

Таким образом, приведенный выше код эквивалентен:

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

Вот что заставляет ваш пример Mutex работать: MutexLock назначается временной безымянной ячейке памяти! Вот где он живет. Посмотрим:

&mut *x.lock().unwrap();

Часть x.lock().unwrap() является выражением значения: она имеет тип MutexLock и возвращается функцией (unwrap()), как и get_foo() выше. Тогда остается только один последний вопрос: является ли операнд оператора deref * контекстом места? Я не упомянул это в списке конкурсов мест выше...

Неявные заимствования

Последняя часть головоломки — неявные заимствования. Из ссылки:

Некоторые выражения будут рассматривать выражение как выражение места, неявно заимствуя его.

К ним относится «операнд оператора разыменования (*)»! И все операнды любого неявного заимствования являются контекстами места!

Итак, поскольку *x.lock().unwrap() выполняет неявное заимствование, операнд x.lock().unwrap() является контекстом места, но поскольку наш фактический операнд - это не место, а выражение значения, он назначается безымянной ячейке памяти!

Почему это не работает для deref_mut()

Есть важная деталь «временных жизней». Еще раз обратимся к цитате:

При использовании выражения значения в большинстве контекстов выражения места создается временная неименованная ячейка памяти, инициализированная этим значением, и вместо этого выражение вычисляется по этой ячейке [...].

В зависимости от ситуации 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, мы получим ошибку "не живет достаточно долго".

Это, конечно же, относится и к x.lock().unwrap().deref_mut() — вот почему мы получаем ошибку.

В случае оператора deref (*) временное существование окружающего блока (эквивалентно привязке let). Могу только предположить, что это особый случай в компиляторе: компилятор знает, что вызов deref() или deref_mut() всегда возвращает ссылку на получателя self, поэтому не имеет смысла заимствовать временное только для вызова функции.

person Lukas Kalbertodt    schedule 14.07.2018
comment
Но здесь время жизни временного кажется вмещающим блоком, а не самым внутренним вмещающим утверждением? - person CodesInChaos; 14.07.2018
comment
@CodesInChaos Спасибо, что поймали это! Я торопился и пропустил эту важную деталь. Я исправил свой ответ, и теперь (почти) все имеет смысл! - person Lukas Kalbertodt; 14.07.2018

Вот мои мысли:

let y: &mut Vec<_> = &mut *x.lock().unwrap();

Несколько вещей, происходящих под поверхностью вашего текущего кода:

  1. Ваш .lock() дает LockResult<MutexGuard<Vec>>
  2. Вы позвонили unwrap() по телефону LockResult и получили MutexGuard<Vec>
  3. Поскольку MutexGuard<T> реализует интерфейс DerefMut, Rust выполняет принудительное отключение ссылок. Он разыменовывается оператором * и дает &mut Vec.

Я считаю, что в Rust вы не вызываете deref_mut самостоятельно, а компилятор выполнит Deref принуждение за вас.

Если вы хотите получить свой MutexGuard, вы не должны разыменовывать его:

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

Из того, что я видел в Интернете, люди обычно делают MutexGuard явным, сохраняя его в переменной и разыменовывая его, когда он используется, как мой модифицированный код выше. Я не думаю, что существует официальная схема по этому поводу. Иногда это также спасает вас от создания временной переменной.

person Xu Chen    schedule 14.07.2018