通过引用将值传递给函数并将其传递给#34; by Box"之间的区别是什么:
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
不是stack_a
,println!
语句后foo()
语句中仍然可以使用println!("{}", stack_a);
?
取消注释error[E0502]: cannot borrow `stack_a` as immutable because it is also borrowed as mutable
--> src/main.rs:10:20
|
8 | let r = foo2(&mut stack_a);
| ------- mutable borrow occurs here
9 | // compile error if the next line is uncommented
10 | println!("{}", stack_a);
| ^^^^^^^ immutable borrow occurs here
...
15 | }
| - mutable borrow ends here
时的错误:
foo
我认为这个错误可以通过参考生命周期来解释。在stack_a
的情况下,main
(在foo
函数中)被移动到函数foo
,但编译器发现函数的参数的生存期{{ 1}},x: &mut i32
,在foo
结束时结束。因此,它允许我们在stack_a
返回后在main
函数中使用变量foo
。在foo2
的情况下,stack_a
也会移到函数中,但我们也会将其返回。
为什么heap_a
的生命周期不会在bar
结束时结束?
答案 0 :(得分:39)
传值始终是副本(如果涉及的类型是“微不足道的”)或移动(如果不是)。 Box<i32>
不可复制,因为它(或至少有一个数据成员)实现Drop
。这通常是针对某种“清理”代码完成的。 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
的函数签名足以得出这个结论。它&#39; S
fn foo2(x: &mut i32) -> &mut i32
实际上是
的缩写fn foo2<'a>(x: &'a mut i32) -> &'a mut i32
根据所谓的“终身省略规则”。此签名的含义是:foo2()
是一个函数,它将一个借来的指针带到某个i32
,并将一个借来的指针返回给i32
i32
(或i32
至少是原始r
的“部分”,因为相同的生命周期参数用于返回类型。只要你坚持那个返回值(stack_a
),编译器会认为{{1}}是可变的借用。
如果您对我们为什么需要在同一时间禁止混叠和(潜在)突变感兴趣?一些内存位置,请查看Niko's great talk。
答案 1 :(得分:19)
当您传递一个盒装值时,您将完全移动该值。你不再拥有它,你传递给它的东西。对于任何不是Copy
的类型都是如此(普通的旧数据只能是memcpy
'd,堆分配当然不能)。这就是Rust的所有权模型的工作方式:每个对象都只在一个地方拥有。
如果您希望改变该框的内容,则应传入&mut i32
而不是整个Box<i32>
。
实际上,Box<T>
仅对递归数据结构有用(因此它们可以表示而不是无限大),并且对于大型类型的非常偶尔的性能优化(如果没有测量,你不应该尝试做。)
要从&mut i32
中获取Box<i32>
,请对取消引用的框进行可变引用,即&mut *heap_a
。
答案 2 :(得分:9)
通过引用传递和“按框”之间的区别在于,在引用情况(“lend”)中,调用者负责释放对象,但是在框中 case(“move”),被调用者负责释放对象。
因此,Box<T>
对于传递负责解除分配的对象很有用,而引用对于传递对象而不负责解除分配非常有用。
一个展示这些想法的简单例子:
fn main() {
let mut heap_a = Box::new(3);
foo(&mut *heap_a);
println!("{}", heap_a);
let heap_b = Box::new(3);
bar(heap_b);
// can't use `heap_b`. `heap_b` has been deallocated at the end of `bar`
// println!("{}", heap_b);
} // `heap_a` is destroyed here
fn foo(x: &mut i32) {
*x = 5;
}
fn bar(mut x: Box<i32>) {
*x = 5;
} // heap_b (now `x`) is deallocated here