感觉Rust官方的学习文档里关于ownship,borrow和lifetime介绍的太简略了,无法真正理解这些语法设计的原因以及如何使用(特别是lifetime)。所以找了一些相关的blog来看,总结一下,以备以后参考。

起因

Rust想要解决的问题是在无GC的情况下安全地管理资源。这点并不容易实现,但不是一点思路都没有。比如,有一个Java程序:

public void foo() {
    byte[] a = new byte[10000000];
   a = null;
  byte[] c = new byte[10000];
}

上边的代码有两处位置是我们可以明确告诉编译器可以释放之前分配的两个数组所占的内存的:

  • a = null  此处,之前a指向的数组不能再被访问,所以它的内存可以被回收了
  • foo方法的结尾 }. 此时,数组c的内存可以被释放。

但是,实际情况会比这更复杂。比如,我们可能在foo方法中把a数组传递给foo的本地作用域之外的数组结构,这样, a就不应该在foo的结尾处被释放了。对于Java,在运行时通过GC的方式回收资源是唯一可行的方式,Java的语法并没有提供足够多的线索使得编译器可以知道内存释放的时机。但这样并不是没有好处,因为如果要添加有利于编译器的标记,就只能由程序员来做,这样无疑会降低程序开发的效率。

在Rust语言里,程序员需要思考资源的使用情况,并提供有关的信息给编译器,以使得编译器在编译时检查资源访问的冲突、以及由编译器来决定资源释放的时机。于是,Rust有了下面三个主流语言没有的语法:

  • ownship
  • borrowing
  • lifetime

下面来概述一下为什么需要这三个语法,它们分别负责解决什么问题。

ownship

首先,如果由编译器来决定什么时候资源应该被销毁,那么编译器依据的规则必须是一个很简单的、不由运行时逻辑决定的规则,比如,Reference Counting这种规则是不能在编译时用来检查的。Rust选择通过scope/stack的方式来决定资源的生命周期。当一个变量离开了它的作用域,它所拥有的资源会被释放。但是如果允许一个资源被多个变量拥有,那么编译器就又得通过非常复杂的方式来决定资源释放的时机、甚至不可能做到这点。所以Rust规定任何资源只能在一个所有者(owner)。这样编译器只用检查资源的owner的作用域,就可以决定资源的释放时机。

move

如果资源在被绑定到它的owner以后,这种“所有权”无法转移,会是非常不灵活的。最重要的情况是我们无法由一个函数来决定其参数绑定的资源的释放。所以,需要“move"语法,来转移资源的所有权。

borrow

如果只能通过move才能使得我们通过函数参数或者非owner的其它变量来访问资源,也会有很多不方便之处:

  • 无法共享一个资源给多个对象
  • 在调用一个函数之后,被move给它的参数的资源之前绑定变量就不能再被使用。很多时候,我们并不想这么做,而只是通用一个函数修改/读取这个资源,在此之后,还想继续使用它之前绑定到的变量。

所以,需要有一个语法允许owner把自己的拥有权“借出”,这个语法就是"borrow"。

但是,这种“借出”比move语法要灵活,比如允许多个变量都能引用到一个资源,但这样就面临着读写冲突的问题。所以borrow分了两种:immutable borrow和mutable borrow,并且编译器对于一个作用域里这两种borrow的数量进行限制,从而避免读写的冲突。

borrow实际上创建了到原始资源的reference,它是一种指针。

比较特殊的是mutable borrow,即&mut,它可以把owner绑定到新的资源。在通过mutable borrow改变owner绑定的目标时,会触发owner最初绑定资源的释放。

lifetime

如果资源(a)的生命周期比引用(b)的短,即在b失效之前,a已经不能再访问了,那么,编译器应该禁止让b引用a,否则会产生“use after free error”。有时候,a和b的这种关系比较容易编译器发现,比如

let a;
{
   let b = 1;
   a = &b
}
println!("{}",a);

但有时候, 这种关系是编译器发现不了的。比如,a是一个函数的返回值,它的生命周期可能比引用b的要短,也可能是一个常量。编译器不去执行函数的逻辑,就无法确定a的生命周期,因此它就无法判断是否使用b来引用a是安全的。所以,Rust需要一些额外的标记,来告诉编译器什么情况下“reference”是可安全访问的。

实际上,Rust中每个reference都有一个相关联的lifetime。不过,程序员无法具体地描述一个reference的lifetime,比如,你无法说"a的生命周期是从第5行到第8行”。"lifetime"的值,最初肯定是由编译器写入的。程序员只能通过'a这种标记来引用已有的lifetime值,来在程序员告诉编译器一些跟lifetime有关的逻辑。

ownship

Variable bindings have a property in Rust: they ‘have ownership’ of what they’re bound to. This means that when a binding goes out of scope, Rust will free the bound resources. For example:

fn foo() {
    let v = vec![1, 2, 3];
}

重点是当一个变量离开它的作用域时,Rust会释放它所绑定的资源。而这个决定了资源生命周期的变量就是这个资源的owner.

Rust会确保一个资源只有一个owner。这个看起来跟读写冲突有关,可以看下The details。而且只有一个owner,编译器显然也更容易确定资源释放的时机。

但是,如果这种资源“所有权"不能转移,就会存在很多问题。比如,我们很多情况下想要将资源的所有权交由一个函数处理。

这种逻辑,由Rust的"move"语法来搞定。

Move semantics

move的特点就是在move之后,原来的变量就不可用了。因为函数就像是一个黑盒,如果把所有权转交给函数,那么无法确保函数返回后之前的变量还能够使用。

move的这个特点,有两种典型的例子可以展示:

let v = vec![1, 2, 3];

let v2 = v;

println!("v[0] is: {}", v[0]);

这种情况下,把vector的所有权move给v2之后,就不可以再访问v了。所以编译时会报错

error: use of moved value: `v`
println!("v[0] is: {}", v[0]);

第二种是把资源move给函数

fn take(v: Vec<i32>) {
    // what happens here isn’t important.
}

let v = vec![1, 2, 3];

take(v);

println!("v[0] is: {}", v[0]);

也会报跟上面一样的错误。

Borrow

如果只能通过资源所绑定到的变量来访问它,会有很多不方便的地方,比如并行地去读取一个变量的值。而且,如果只想“借用”一下某个变量绑定的资源,在借用完成以后,不想释放这个资源,而是把所有权“交还”给原来的变量,那么用move语法就有些不方便。比如Rust文档里的这个例子:

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with v1 and v2

    // hand back ownership, and the result of our function
    (v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);

这里,foo函数结束时并不想释放v1、v2变量绑定的资源,而是想继续使用他们。如果只有move语法,就只能用函数返回值的方式返回所有权。

borrow语法,可以使这种情况更简单。但是,它本身也会带来新的复杂性。

上面的例子,用borrow语法,可以这么做:

fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
    // do stuff with v1 and v2

    // return the answer
    42
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let answer = foo(&v1, &v2);

// we can use v1 and v2 here!

Instead of taking Vec<i32>s as our arguments, we take a reference: &Vec<i32>. And instead of passing v1 and v2 directly, we pass &v1 and &v2. We call the &T type a ‘reference’, and rather than owning the resource, it borrows ownership. A binding that borrows something does not deallocate the resource when it goes out of scope. This means that after the call to foo(), we can use our original bindings again.

所以,borrow实际上就是生成了对资源的引用,这种引用的作用域并不和资源的生命周期挂钩,这点和binding有本质的不同。

下面,要明确的就是“borrow"的范围,就是从什么时候开始borrow,到什么时候borrow结束。

看下面的例子:

let mut x = 5;
{
    let y = &mut x; //borrow开始
    *y += 1;
} //borrow结束
println!("{}", x);

之所以要确定borrow的范围,是因为borrow语法有一些跟作用域要关的要求:

  • First, any borrow must last for a scope no greater than that of the owner.
  • Second, you may have one or the other of these two kinds of borrows, but not both at the same time:
    •   one or more references (&T) to a resource,
    •   exactly one mutable reference (&mut T).

第一,当owner无法访问了,那么borrow一定不能再访问。

第二,下面两种情况只能存在一种:

  • 对资源的一个或多个不可变的引用(&T)
  • 对资源的唯一一个可变的引用(&mut T), 也就是说不能同时有多个可变引用。

第二个限制,是为了防止读写冲突。特别是一个mutable borrowing,可能会使得对同一个资源的immutable borrowing访问错误的地址,当然也可能会使得其它的mutable borrowing访问错误的地址。

比如:

fn main() {
    let mut x = 5;

    let y = &mut x;    // -+ &mut borrow of x starts here
                       //  |
    *y += 1;           //  |
                       //  |
    println!("{}", x); // -+ - try to borrow x here
}                      // -+ &mut borrow of x ends here
      

上边的这段代码,编译时就会报错:“cannot borrow `x` as immutable because it is also borrowed as mutable"

而第一个限制是很容易理解的,毕竟如果owner都访问不了了,那么reference当然就不能用了。下面是一个例子:

let y: &i32;
{
    let x = 5;
    y = &x;
}

println!("{}", y);

在x的作用域结束后,对它的borrow y还可以访问,所以,以上的代码不会通过编译。

参考文档

The Rust Programming Language

Explore the ownership system in Rust

Rust Borrow and Lifetimes

Lifetime Parameters in Rust

Rust Lifetimes

Rust: move和borrow的更多相关文章

  1. Rust: lifetime

    Rust的lifetime算是它最重要的特性之一,也不大好理解,特别是官方文档的介绍有些太过简略,容易让人误解. 这篇文章: Rust Lifetimes 应该可以解答很多人疑惑,特别是有关lifet ...

  2. Rust所有权及引用

    Rust 所有权和借用 Rust之所以可以成为万众瞩目的语言, 就是因为其内存安全性. 在以往内存安全几乎全都是通过GC的方式实现, 但是GC会引来性能.CPU以及Stop The World等问题, ...

  3. Facebook币Libra学习-6.发行属于自己的代币Token案例(含源码)

    在这个简短的概述中,我们描述了我们在eToro标记化资产背后实施技术的初步经验,即MoveIR语言中的(eToken),用于在Libra网络上进行部署. Libra协议是一个确定性状态机,它将数据存储 ...

  4. [源码解析] TensorFlow 分布式环境(5) --- Session

    [源码解析] TensorFlow 分布式环境(5) --- Session 目录 [源码解析] TensorFlow 分布式环境(5) --- Session 1. 概述 1.1 Session 分 ...

  5. rust borrow and move

    extern crate core; #[deriving(Show)] struct Foo { f : Box<int> } fn main(){ let mut a = Foo {f ...

  6. Rust入门篇 (1)

    Rust入门篇 声明: 本文是在参考 The Rust Programming Language 和 Rust官方教程 中文版 写的. 个人学习用 再PS. 目录这东东果然是必须的... 找个时间生成 ...

  7. A First Look at Rust Language

    文 Akisann@CNblogs / zhaihj@Github 本篇文章同时发布在Github上:http://zhaihj.github.io/a-first-look-at-rust.html ...

  8. 【转】对 Rust 语言的分析

    对 Rust 语言的分析 Rust 是一门最近比较热的语言,有很多人问过我对 Rust 的看法.由于我本人是一个语言专家,实现过几乎所有的语言特性,所以我不认为任何一种语言是新的.任何“新语言”对我来 ...

  9. 2.4 Rust Ownership

    What Is Ownership ownership这个单词有些不好翻译,刚开始就直接叫它“ownership”即可.这里简单说一下,我对它的理解, 从“数据结构与算法”的角度来看,ownershi ...

随机推荐

  1. Dll学习二_Dll 窗体中动态创建数据并使用Demo

    沿用上一篇Demo 环境:DelphiXE,XP,SQL2005 贴出改动过的单元代码: dbGrid控件版: unit SubMain_Unit; interface uses Windows, M ...

  2. JDBC基础一

    JDBC:java database connectivity SUN公司提供的一套操作数据库的标准规范. JDBC与数据库驱动的关系:接口与实现的关系. JDBC规范(掌握四个核心对象): Driv ...

  3. C语言中进制知识总结

    1.什么是进制 进制是一种计数的方式,常用的有二进制.八进制.十进制.十六进制.任何数据在计算机内存中都是以二进制的形式存放的. 我对进制的个人理解,二进制数是以2为计算单元,满2进1位的数:八进制数 ...

  4. Mongodb shell 基本操作

    /opt/mongodb-2.6.6/bin > mongo 1. 查询本地所有数据库名称> show dbs 2. 切换至指定数据库环境(若无指定的数据库,则创建新的库)> use ...

  5. linq to sql (Group By/Having/Count/Sum/Min/Max/Avg操作符)

    Group By/Having操作符 适用场景:分组数据,为我们查找数据缩小范围. 说明:分配并返回对传入参数进行分组操作后的可枚举对象.分组:延迟 1.简单形式: var q = from p in ...

  6. C#(HTML)_小技巧_关于textbox框中不能输入HTML标签的解决方法(如输入“<p>”后,在提交表单时系统会崩溃)

    主要修改文件是config文件(Web.config): 1.在<pages>标签中添加属性:validateRequest="false" <pages val ...

  7. listview 优化

    ListView的优化: (前两点都是利用ListView的自身优化机制优化[缓存优化]) 1.利用ListView自身的缓存机制,他会缓存条目中的一个条目item,当listview第一屏显示完成之 ...

  8. layer 弹出子页面然后给父页面赋值

    //----赋值 并关闭当前页面 开始---- FunctionActionDeleteXZ = function (CompanyId, RelCompanyName) { parent.$(&qu ...

  9. 59.DDR3_IP核文件设置

    在ISE软件生成DDR3 IP核时,会产生很多文件,其中user_design,example_design里面分别是用户接口文件和自带的仿真测试文件.在user_design里的rtl中,这些文件是 ...

  10. linux校准时间

    Linux下ntpdate时间同步 Ntp服务器安装配置 ntp(Network Time Protocol)协议 RedHat服务器可以下载rpm安装包,然后执行# rpm -ivh ntp-4.2 ...