Подтвердить что ты не робот

Разница между передачей по ссылке и по ящику

В чем разница между передачей значения функции по ссылке и ее передачей по полю:

fn main() {
    let mut stack_a = 3;
    let mut heap_a = Box::new(3);

    foo(&mut stack_a);
    println!("{}", stack_a);

    let r = foo2(&mut stack_a);
    // compile error if the next line is uncommented
    // println!("{}", stack_a);

    bar(heap_a);
    // compile error if the next line is uncommented
    // println!("{}", heap_a);
}

fn foo(x: &mut i32) {
    *x = 5;
}

fn foo2(x: &mut i32) -> &mut i32 {
    *x = 5;
    x
}

fn bar(mut x: Box<i32>) {
    *x = 5;
}

Почему heap_a перемещается в эту функцию, но stack_a is not (stack_a по-прежнему доступен в инструкции println! после вызова foo())?

Ошибка при раскомментировании второй строки:

error[E0382]: use of moved value: `heap_a`
  --> <anon>:14:20
   |
12 |     bar(heap_a);
   |         ------ value moved here
13 |     // compile error if the next line is uncommented
14 |     println!("{}", heap_a);
   |                    ^^^^^^ value used here after move
   |
   = note: move occurs because `heap_a` has type `std::boxed::Box<i32>`, which does not implement the `Copy` trait

Я предполагаю, что это как-то зависит от времени жизни, потому что в случае foo2, stack_a также перемещается в функцию. Если мое предположение верно, то в случае foo, stack_a также переходит к функции, но rustc находит, что его время жизни заканчивается в конце foo и позволяет использовать stack_a после окончания foo. Тогда почему срок жизни heap_a не заканчивается в конце bar?

4b9b3361

Ответ 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. некоторые места памяти, проверьте Нико отличный разговор.

Ответ 2

Когда вы передаете значение в штучной упаковке, вы полностью переносите значение. Вы больше не владеете им, то, что вы его передали. Это так для любого типа, который не является Copy (простые старые данные, которые могут быть только memcpy d, что, конечно, распределение кучи не может быть). Так работает модель владения Rusts: каждый объект принадлежит только одному месту.

Если вы хотите изменить содержимое поля, вы должны передать &mut i32, а не весь Box<i32>.

Действительно, Box<T> полезен только для рекурсивных структур данных (так что они могут быть представлены, а не бесконечного размера) и для очень редкой оптимизации производительности на больших типах (которые вы не должны делать без измерений).

Чтобы получить &mut i32 из Box<i32>, возьмите измененную ссылку на поле разыменования, т.е. &mut *heap_a.

Ответ 3

Как следует из "Niko great talk" (из ответа sellibitze), разница между передачей по ссылке и "по ящику" заключается в том, что: в справочном примере ( "кредитовать" ) вызывающий абонент несет ответственность за освобождение объекта, но в случае коробки ( "переместить" ) вызывающий отвечает за освобождение объекта. Поэтому Box<T> полезно для передачи объектов, несущих ответственность за освобождение, в то время как ссылка полезна для передачи объектов без ответственности за освобождение.

Простой пример:

fn main() {
    let mut heap_a = box 3i;
    foo(&mut *heap_a);
    println!("{}", heap_a);

    let mut heap_b = box 3i;
    bar(heap_b);
    // can't use heap_b. heap_b has been deallocated at end of "bar"
    //println!("{}", heap_b); 
} // heap_a is destroyed here

fn foo(x: &mut int){
    *x = 5;
}

fn bar(mut x: Box<int>){
    *x = 5;
} // heap_b (now - x) is deallocated here