Ответ 1
Передача по значению всегда является либо копией (если задействованный тип является "тривиальным" ), либо перемещением (если нет). Box<i32>
не копируется, потому что он (или, по крайней мере, один из его элементов данных) реализует Drop
. Обычно это делается для какого-то "очищающего" кода. A Box<i32>
является "владеющим указателем". Это единственный владелец того, на что он указывает, и почему он "чувствует ответственность", чтобы освободить память i32
в своей функции Drop
. Представьте, что произойдет, если вы скопировали Box<i32>
: теперь у вас будет два экземпляра Box<i32>
, указывающие на то же место в памяти. Это было бы плохо, потому что это привело бы к двойной ошибке. Поэтому bar(heap_a)
перемещает экземпляр Box<i32>
в bar()
. Таким образом, всегда существует не более одного владельца выделенного кучи i32
. И это упрощает управление памятью: тот, кто ее владеет, освобождает его в конце концов.
Разница в foo(&mut stack_a)
заключается в том, что вы не проходите stack_a
по значению. Вы просто "одалживаете" foo()
stack_a
таким образом, чтобы foo()
мог его мутировать. То, что foo()
получает, является заимствованным указателем. Когда выполнение возвращается из foo()
, stack_a
все еще существует (возможно, изменено через foo()
). Вы можете думать об этом как stack_a
, возвращаемом в свой собственный стек стека, потому что foo()
просто заимствовал его только на время.
Часть, которая вас смущает, заключается в том, что, раскомментируя последнюю строку
let r = foo2(&mut stack_a);
// compile error if uncomment next line
// println!("{}", stack_a);
вы фактически не проверяете, был ли перемещен stack_a
. stack_a
все еще существует. Компилятор просто не позволяет вам получить к нему доступ через свое имя, потому что у вас все еще есть смену с заимствованной ссылкой: r
. Это одно из правил, которые нам необходимы для обеспечения безопасности памяти: может быть только один способ доступа к ячейке памяти, если нам также разрешено ее изменять. В этом примере r
является взаимозаменяемой ссылкой на stack_a
. Таким образом, stack_a
по-прежнему считается взаимозаменяемым. Единственный способ получить доступ к нему - через заимствованную ссылку r
.
С помощью некоторых дополнительных фигурных скобок мы можем ограничить время жизни этой заимствованной ссылки r
:
let mut stack_a = 3;
{
let r = foo2(&mut stack_a);
// println!("{}", stack_a); WOULD BE AN ERROR
println!("{}", *r); // Fine!
} // <-- borrowing ends here, r ceases to exist
// No aliasing anymore => we're allowed to use the name stack_a again
println!("{}", stack_a);
После закрывающей скобки снова появляется только один способ доступа к ячейке памяти: имя stack_a
. Поэтому компилятор позволяет использовать его в println!
.
Теперь вы можете задаться вопросом, как компилятор знает, что r
на самом деле ссылается на stack_a
? Проводит ли он анализ реализации foo2
для этого? Нет. Нет необходимости. Функциональной сигнатуры foo2
достаточно для достижения этого вывода. Это
fn foo2(x: &mut i32) -> &mut i32
что на самом деле является коротким для
fn foo2<'a>(x: &'a mut i32) -> &'a mut i32
в соответствии с так называемыми "правилами элиты жизни". Значение этой подписи таково: foo2()
- это функция, которая берет заимствованный указатель на некоторый i32
и возвращает заимствованный указатель на i32
, который является тем же самым i32
(или, по крайней мере, "частью" original i32
), поскольку для возвращаемого типа используется тот же самый параметр времени жизни. Пока вы держитесь за это возвращаемое значение (r
), компилятор считает stack_a
изменчиво заимствованным.
Если вас интересует, почему нам нужно запретить сглаживание и (потенциальную) мутацию, происходящую одновременно с w.r.t. некоторые места памяти, проверьте Нико отличный разговор.