通过引用将值传递给函数并通过Box传递它有什么区别?

时间:2014-12-04 22:42:50

标签: rust

通过引用将值传递给函数并将其传递给#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_aprintln!语句后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结束时结束?

3 个答案:

答案 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