Rust: move和borrow
感觉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 passingv1andv2directly, we pass&v1and&v2. We call the&Ttype 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 tofoo(), 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).
- one or more references (
第一,当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的更多相关文章
- Rust: lifetime
Rust的lifetime算是它最重要的特性之一,也不大好理解,特别是官方文档的介绍有些太过简略,容易让人误解. 这篇文章: Rust Lifetimes 应该可以解答很多人疑惑,特别是有关lifet ...
- Rust所有权及引用
Rust 所有权和借用 Rust之所以可以成为万众瞩目的语言, 就是因为其内存安全性. 在以往内存安全几乎全都是通过GC的方式实现, 但是GC会引来性能.CPU以及Stop The World等问题, ...
- Facebook币Libra学习-6.发行属于自己的代币Token案例(含源码)
在这个简短的概述中,我们描述了我们在eToro标记化资产背后实施技术的初步经验,即MoveIR语言中的(eToken),用于在Libra网络上进行部署. Libra协议是一个确定性状态机,它将数据存储 ...
- [源码解析] TensorFlow 分布式环境(5) --- Session
[源码解析] TensorFlow 分布式环境(5) --- Session 目录 [源码解析] TensorFlow 分布式环境(5) --- Session 1. 概述 1.1 Session 分 ...
- rust borrow and move
extern crate core; #[deriving(Show)] struct Foo { f : Box<int> } fn main(){ let mut a = Foo {f ...
- Rust入门篇 (1)
Rust入门篇 声明: 本文是在参考 The Rust Programming Language 和 Rust官方教程 中文版 写的. 个人学习用 再PS. 目录这东东果然是必须的... 找个时间生成 ...
- A First Look at Rust Language
文 Akisann@CNblogs / zhaihj@Github 本篇文章同时发布在Github上:http://zhaihj.github.io/a-first-look-at-rust.html ...
- 【转】对 Rust 语言的分析
对 Rust 语言的分析 Rust 是一门最近比较热的语言,有很多人问过我对 Rust 的看法.由于我本人是一个语言专家,实现过几乎所有的语言特性,所以我不认为任何一种语言是新的.任何“新语言”对我来 ...
- 2.4 Rust Ownership
What Is Ownership ownership这个单词有些不好翻译,刚开始就直接叫它“ownership”即可.这里简单说一下,我对它的理解, 从“数据结构与算法”的角度来看,ownershi ...
随机推荐
- ok6410内存初始化
•DRAM:它的基本原件是小电容,电容可以在两个极板上保留电荷,但是需要定期的充电(刷新),否则数据会丢失.缺点:由于要定期刷新存储介质,存取速度较慢. •SRAM:它是一种具有静止存取功能的内存,不 ...
- 解决 MVC 用户上线下线状态问题
以前工作项目中就有一个微博类功能,其中也出现了用户在线和离线的问题. 但是因为初入程序猿 使用的是 Session _end 上个事件. Session _end 这个事件不需要怎么解释吧 就是在se ...
- 如何在windows下安装python第三方包
python安装第三方库一般方式和easy_install方式 2010-06-24 17:43:53| 分类: Python | 标签:python |字号 订阅 python安装第三 ...
- wpf mvvm MenuItem的Command事件
这是一个事件的辅助类,可以通过它实现MenuItem的Command事件 public class MyCommands : Freezable, ICommand, ICommandSource { ...
- 菜鸟学习Hibernate——多对多关系映射
Hibernate中的关系映射,最常见的关系映射之一就是多对多关系映射例如用户与角色的关系,一个用户对应多个角色,一个角色对应多个用户.如图: Hibernate中如何来映射这两个的关系呢? 下面就为 ...
- 6.24 AppCan移动开发者大会:议程重大更新,报名即将关闭
大会倒计时2天,议程重大更新,报名通道即将关闭! 创业6年,由AppCan主办的第一届移动开发者大会将在本周五盛大召开.超过100万开发者线上参与.现场1500人规模.50家移动互联企业深度参与.30 ...
- 【js类库AngularJs】web前端的mvc框架angularjs之hello world
AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀的前端JS框架,已经被用于Google的多款产品当中.AngularJS有着诸多特性,最为核 ...
- SQL Server Analysis Services 数据挖掘
假如你有一个购物类的网站,那么你如何给你的客户来推荐产品呢?这个功能在很多 电商类网站都有,那么,通过SQL Server Analysis Services的数据挖掘功能,你也可以轻松的来构建类似的 ...
- MongoDB的交互(mongodb/node-mongodb-native)、MongoDB入门
MongoDB 开源,高性能的NoSQL数据库:支持索引.集群.复制和故障转移.各种语言的驱动程序:高伸缩性: NoSQL毕竟还处于发展阶段,也有说它的各种问题的:http://coolshell.c ...
- 读:HIS 与医保系统的接入方案及实现
HIS 与医保系统的接入方案及实现刘剑锋 李刚荣第三军医大学西南医院信息科(重庆 400038) 医院HIS和医保系统的接口设计方案涉及两个部分,分别由医院和医保中心分别完成相,应的程序设计,这两部分 ...