Rust租借和生命周期深入剖析

资源的所有权和租借

Rust通过一个成熟的租借系统而不是GC来达到内存安全的目的。对于和种资源(栈内存,堆内存,文件句柄等),都确定只有一个拥有者来确保其正确的解构(如果资源需要解构的话)。你可以利用&或者&mut创建对资源新的绑定,我们把这种绑定叫做租借。编译器会保证所有的所有者和租借都正常工作。

复制和所有权转移(Move)

在我们进入租借系统的讨论前,我们还知道,Rust会处理复制和所有权移动。基本上,在赋值和函数调用里面:

  1. 如果一个值是可以复制的(只有原生基本类型,不涉及其他如内存和文件句柄等资源),编译器默认是复制其值。
  2. 除此之外,编译器会转移其所有权,并让原先的绑定失效。

长话短说,也就是原生基本类型=>复制,非原生基本类型=>转移所有权。

Rust的复制跟C一样,所有原生基本类型是字节复制(浅内存拷贝)而不是语义性的深拷贝。

转移了所有权之后,资源就被下一个拥有者拥有

资源析构

当资源的所有权不在的时候,Rust会立刻释放该资源。也就是说,当:

  1. 拥有者离开作用域,或者
  2. 所有权的绑定发生变化(原有所有权的绑定失效)

拥有者和租借者的特权和限制

拥有者有一些特权,它可以:

  1. 控制资源的析构。
  2. 出租资源,不可变租借(可以同时有多个)和可变租借(排他的,只能有一个可变租借)
  3. 移交所有权(move语义)

拥有者同时也有一些限制:

  1. 在一个租借过程中,拥有者不可以改变资源,或者出借一个可变租借
  2. 在一个可变租借过程中,拥有者不可以访问资源,或者再进行租借。

租借者也有一些特权。除了能够访问和修改被租借的资源,租借者还能够共享租借:

  1. 一个租借者可以共享(复制)一个不可变租借。
  2. 一个可变租借可以转移可变租借。(可变租借是默认转移的)

代码样例

谈得够多的了,让我们来看些代码。在下面的例子中,我们将会使用一个不可复制的结构体Foo(包含了一个在Box里面的值,也就是说分配在堆里),使用不可复制资源会让操作变得更加受限,有利于学习。

下面每个例子,我们都提供一个"作用域图"来说明所有者的作用域,对于租借也是一样。第一行的大括号对应代码里面的大括号。

拥有者在可变租借过程中不能访问资源

struct Foo {  
    f: Box<u32>,
}

fn main() {  
    let mut a = Foo { f: Box::new(10) };
    // mutable borrow
    let x = &mut a;

    // 错误: cannot borrow `a.f` as immutable because `a` is also borrowed as mutable
    println!("{}", a.f);
}
          { a x * }
拥有者 a   |-----|
租借者 x     |---| x = &mut a
访问 a.f       |   error

这违背了拥有者的限制第二条。如果我们将let x = &mut a;放在一个嵌套的代码块里,租借在println!之前就结束了,那么代码可以通过编译运行:

struct Foo {  
    f: Box<u32>,
}

fn main() {  
    let mut a = Foo { f: Box::new(10) };
    // mutable borrow
    {
        let x = &mut a;
    }

    println!("{}", a.f);
}
          { a { x } * }
拥有者 a   |---------|
租借者 x       |-|     x = &mut a
访问 a.f            |   OK

租借者能够将可变租借转移给新的租借者
fn main() {  
    let mut a = Foo { f: Box::new(10) };
    // mutable borrow
    let x = &mut a;
    // move the mutable borrow to new borrower y
    let y = x;
    // error: use of moved value: `x.f`
    println!("{}", x.f);
}
         { a x y * }
拥有者 a   |-------|
租借者 x     |-|     x = &mut a
租借者 y       |---| y = x
访问 x.f         |   error

在转移之后,原先的租借者x不能再继续访问租借的资源。

租借域

当我们开始传递引用(&和&mut)时,事情开始变得有趣了。这也是经常让很多Rust初学者迷惑的地方。

生命周期

在整个租借过程中,非常重要的一点是知道一个租借者的租借开始和结束的地方。在这里,我们用"租借域"来描述一个租借可用的作用域。注意我们这里的定义和Rust定义的生命周期是有点区别的,后面详说。

& = 租借

首先,让我们记住&等于一个租借,而&mut相当于一个可变租借。

另外,当一个&出现在任何结构体(在其字段中出现),或者函数/闭包(在其返回类型或者参数里面),这个结构体/函数/装饰就是一个租借者,并且要遵守所有的租借规则。

最后,对于所有租借,只有唯一一个拥有者,和一个(可变租借)或者多个(不可变租借)租借。

租借域扩展

关于租借域的说明:

首先,一个租借域: “是租借可用的作用域 ”租借域不同于最初租借所在的语法作用域,因为租借者可以扩展其租借域(见下文)

另外,一个租借者可以在赋值或者函数调用中通过复制(不可变租借)或者转移(可变租借)来扩展租借域。租借接收者(可以是一个新的绑定,结构体,函数或者闭包)就变成了新的租借者。

最后,租借域是所有租借者语法作用域的并集,并且被租借的资源必须在整个租借域中都可用。

租借准则

最后,我们有这样一条租借准则:

资源作用域 >= 租借域 = 所有租借者的语法作用域

代码例子

让我们来看关于租借域扩展的例子,结构体Foo和上面的例子一样:

fn main() {  
    let mut a = Foo { f: Box::new(10) };
    let y: &Foo;
    if false {
        //租借
        let x = &a;

        //和y共享租借,因此扩展了租借域
        y = x;
    }

    // 错误: cannot assign to `a.f` because it is borrowed
    a.f = Box::new(1);
}
          { a { x y } * }
资源 a      |-----------|
拥有者 x        |---|     x = &a
租借者 y          |-----| y = x
租借域          |=======|
修改 a.f               |   error

即使租借发生在if代码块里面,并且x超出其语法作用域,它通过赋值y = x; 扩展了租借域。所以存在两个租借者x和y。依照租借准则,租借域是租借者x和租借者y的语法作用域并集,所以其范围是从let x = &a; 到main函数的结束(注意绑定y在y=x之前,并不是一个租借者)。

你可能已经注意到if代码块永远不会被执行到,因为它的条件是false,但是编译器还是阻止了a访问它的资源。这是因为租借检查是发生在编译阶段,而不是在代码运行时。

租借多个资源

到现在为止,我们只是关注对单一资源的租借。而一个租借者可能租借多个资源吗?当然可以!例如,一个函数可能接收多个引用,并根据条件返回其中一个:

fn max(x: &Foo, y: &Foo) -> &Foo  

main函数返回一个&引用,因此它是一个租借者。返回的结果是两个参数其中的一个,所以它租借着两个资源。

命名租借域

当有多个&引用作为输入时,我们需要明确利用"命名生命周期"来明确其关系。在这里我们同样称呼为"命名租借域"。

上面的代码是无法通过编译的,因为我们没有明确两个租借者的关系。比如,我们没有说明,哪个租借者是在哪个租借域里面。下面的实现才是合法的:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {  
if x.f > y.f { x } else { y }  
}
(所有的资源和租借者都包含在命名租借域'a中)
            max( {   } )
资源   *x <-------------->
资源   *y <-------------->
租借域 'a <==============>
租借者 x        |___|
租借者 y        |___|
返回值            |___|   pass to the caller

在这个函数中,我们有一个租借域'a和三个租借者:两个输入参数,和函数返回。上面提到的租借准则依旧适用。并且所有租借资源必须遵从该租借准则。让我们看下面的例子。

代码例子

在下面的代码中,我们用上面的max函数来挑出a和b中间大的那个返回:

fn main() {  
    let a = Foo { f: Box::new(10) };
    let y: &Foo;
    if false {
        let b = Foo{ f: Box::new(12) };
        let x = max(&a, &b);
        // error: `b` does not live long enough
        y = x;    //这句注释可通过编译运行
    }
}
       { a { b x (  ) y } }
资源 a   |----------------| pass
资源 b       |----------|   fail
租借域         |==========|
临时租借者        |-|       &a
临时租借者        |-|       &b
租借者 x       |----------|   x = max(&a, &b)
租借者 y              |---| y = x

在let x = max(&a, &b);之前,&a和&b是临时引用,只有当前语句中有效。而第三个租借者x租借了这两个资源(无论a还是b,都被它借去了),直到if代码块结束。所以整个租借域是从let x = max(&a,&b);直到if代码块结束。无论资源a还是b,在这个租借域中都有效,因此满足了租借准则。

当编译到y = x;时,y成为第四个租借者,并将租借域扩展到main代码块结束,而资源b在if语句块结束时就失效了,导致无法通过租借准则的检查。

结构体租借者

相对于函数和闭包,一个结构体也可以通过将引用存储在字段上租借多个资源,来租借多个资源。我们将会通过几个例子来看它是怎么遵守租借准则的。接下来,让我们改用下面的Link结构体来保存一个引用(一个不可变租借)来说明:

struct Link<'a> {  
    link: &'a Foo,
}
结构体租借多个资源

即使只有一个字段,Link结构体也可以租借多个资源:

fn main() {  
    let a = Foo { f: Box::new(12) };
    let mut x = Link { link: &a };
    if false {
        let b = Foo { f: Box::new(10) };
        // error: `b` does not live long enough
        x.link = &b;   //该行注释可通过编译运行
    }
}
       { a x { b * } }
资源 a   |-----------| pass
资源 b         |---|   fail
租借域     |=========|
租借者 x   |---------| x.link = &a
租借者 x         |---| x.link = &b

在上面这个例子中,租借者x从拥有者a处租借资源,所以租借域是直到main代码块结束。而在x.link = &b;语句,x想要去从拥有者b处租借资源,就会因为通过不了租借准则而失败。

无返回值函数扩展租借域

一个没有返回值的函数也能够通过输入函数扩展租借域。例如,函数store_foo接收一个Link的可变引用,将在Link里面存储Foo的一个引用(不可变租借):

fn store_foo<'a>(x: &mut Link<'a>, y: &'a Foo) {  
    x.link = y;
}

在下面的代码中,a拥有的被租借的资源; Link结构体是其租借者,而Link租借体又被x可变引用着(也就是说*x是租借者),租借域是直到main代码块结束。

fn main() {  
    let a = Foo { f: Box::new(12) };
    let x = &mut Link { link: &a };
    if false {
        let b = Foo { f: Box::new(10) };
        store_foo(x, &b)
    }
}
       { a x { b * } }
资源 a   |-----------| pass
资源 b         |---|   fail
租借域     |=========|
租借者 *x  |---------| x.link = &a
租借者 *x        |---| x.link = &b

当程序编译到store_foo(x, &b);时,函数会试图去存储&b到x.link,导致资源b成为另一个被租借的资源,并且在租借准则中检查失败。因为b的语法作用域没有覆盖整个租借域。

多重租借域

在一个函数中拥有多个命名的租借域是可能是。例如:

fn superstore_foo<'a, 'b>(x: &mut Link<'a>, y: &'a Foo,  
                          x2: &mut Link<'b>, y2: &'b Foo) {
    x.link = y;
    x2.link = y2;
}

在这个函数中,涉及到两个不同的租借域。每个租借域各自需要遵守租借准则(各自进行租借准则检查)。

为什么生命周期会让人迷惑

最后,我想要解析下为什么我认为在Rust的租借系统中用到的名词"生命周期"会让人迷惑(所以在这篇文章中我都回避使用它)。

当我们谈论租借时,涉及到几种不同的"生命周期": * A: 资源拥有者的"生命周期"(或者拥有/被租借的资源) * B: 整个租借过程的"生命周期",例如,从第一个租借到最后一个失效 * C: 一个单独的租借者或者租借的指针的"生命周期"

当一个人说"生命周期"时,他可能指的是上面任何一个。如果涉及到多个资源和租借,事情将会变得更加复杂。例如,"命名生命周期"在函数或者结构体定义中指的是什么?A,B,还是C?

在上面max函数中:

fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {  
if x.f > y.f { x } else { y }  
}

在这里生命周期'a指的是什么?它肯定不是A,因为这两个资源拥有者拥有不同的生命周期。它也不可能是C,因为这里有三个租借者: x,y和返回值。并且它们也各自有着不同的生命周期。那么在这里是指B吗?也许吧。而且租借域本身就不是一个具体的实体,它又怎么会有"生命周期"呢?把它称作生命周期是会让人迷惑的。

整个所有权/租借概念本身已经是够复杂的了。而用"生命周期"这样的名词,只会让事情变得更加难以理解。所以我们用"租借域"来明确表达这个概念。

P.S. 利用上面定义的A, B和C,租借准则可以表达为:

A >= B = C1 U C2 U … U C  
学习Rust是非常值得的!

尽管Rust的租借和所有权概念会花费你相当多的时间去掌握,但是这是非常有意思的学习过程。Rust要达到内存安全的目标而不用GC,到目前还做得挺好的。有人说,学习Haskell会改变你编程的思维,我认为,Rust也是一样,非常值得我们花时间去学习的。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值