将值传递给函数并通过"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) { *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
,x: &mut i32
在的结尾处结束foo
.因此,让我们使用变量stack_a
的main
函数后foo
返回.在这种情况下foo2
,stack_a
也移动到函数,但我们也返回它.
为什么heap_a
结尾的生命没有结束bar
?
传递盒装值时,您将完全移动该值.你不再拥有它,你传递给它的东西.对于任何类型都不是这样的Copy
(普通的旧数据只能是memcpy
'd,堆分配当然不能).这就是Rust的所有权模型的工作原理:每个对象都只在一个地方拥有.
如果你想改变盒子的内容,你应该传入&mut i32
而不是整体Box<i32>
.
实际上,Box<T>
它只对递归数据结构很有用(因此它们可以表示而不是无限大小)以及大型类型的非常偶然的性能优化(如果没有测量,您不应该尝试这样做).
为了&mut i32
摆脱a Box<i32>
,对可以解除引用的框进行可变引用,即&mut *heap_a
.
传值始终是副本(如果涉及的类型是"微不足道的")或移动(如果不是).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
(或至少是原始的"部分" 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