将值传递给函数并通过"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;
}
Run Code Online (Sandbox Code Playgroud)
为什么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
Run Code Online (Sandbox Code Playgroud)
我认为这个错误可以通过参考生命周期来解释.在的情况下foo,stack_a(在main功能)被移动到功能foo,但是编译器发现该函数的自变量的生存期foo,x: &mut i32在的结尾处结束foo.因此,让我们使用变量stack_a的main函数后foo返回.在这种情况下foo2,stack_a也移动到函数,但我们也返回它.
为什么heap_a结尾的生命没有结束bar?
sel*_*tze 42
传值始终是副本(如果涉及的类型是"微不足道的")或移动(如果不是).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);
Run Code Online (Sandbox Code Playgroud)
你实际上并没有测试是否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);
Run Code Online (Sandbox Code Playgroud)
在结束括号之后,再次只有一种方式访问内存位置:名称stack_a.这就是编译器允许我们使用它的原因println!.
现在您可能想知道,编译器如何知道r实际指的是stack_a什么?它是否分析了它的实施foo2?不,没有必要.功能签名foo2足以得出这个结论.它的
fn foo2(x: &mut i32) -> &mut i32
Run Code Online (Sandbox Code Playgroud)
这实际上是短暂的
fn foo2<'a>(x: &'a mut i32) -> &'a mut i32
Run Code Online (Sandbox Code Playgroud)
根据所谓的"终身省略规则".这个签名的含义是:foo2()是一个函数,它将一个借来的指针带到某个函数,并将一个借来的指针i32返回到一个i32相同的i32(或至少是原始的"部分" i32),因为相同的生命周期参数用于返回类型.只要你坚持那个返回值(r),编译器会考虑stack_a可变地借用.
如果您对我们为什么需要在某些内存位置同时禁止混叠和(潜在)突变感兴趣,请查看Niko的精彩演讲.
传递引用和"按框"之间的区别在于,在引用案例("lend")中,调用者负责解除对象的释放,但在盒子情况下("移动"),被调用者负责解除分配物体.
因此,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
Run Code Online (Sandbox Code Playgroud)