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 ...
随机推荐
- 在GridControl控件中使用SearchLookUpEdit构建数据快速输入
较早之前,曾经介绍了一篇文章<使用DataGridView数据窗口控件,构建用户快速输入体验>,介绍了在传统DataGridView中嵌入一个数据窗口进行选择列表,从而实现数据快速录入的操 ...
- 记录C++学习历程
从今天开始学习C++,将学习中遇到的问题,以及解决方案记录在这个博客里. 函数 1.C++函数声明(原型) 函数原型跟函数的定义在返回值类型,函数名,参数上必须完全一致. 2.程序的内存区域:全局数据 ...
- 从客户端检测到有潜在危险的Request.Form 值【转】
asp.net开发中,经常遇到“从客户端检测到有潜在危险的Request.Form 值”错误提示,很多人给出的解决方案是: 1.web.config文档<system.web>后面加入这一 ...
- 压力测试之TCPP
1.下载源码 tpcc-mysql-src.tgz 2.解压 tpcc-mysql-src.tgz 3.安装 [root@DBMysql mysql]# cd /home/mysql/tpcc-mys ...
- 程序员定制的中州韵(rime)windows版(小狼毫)微软双拼输入法
小狼毫所有的配置都是在用户文件夹下完成的 用户文件夹在win7的开始菜单的小狼毫文件夹中可以找到 所有设置希望生效须用小狼毫开始菜单中的重新部署来更新配置 -> weasel.custom.ya ...
- 【转】 BSS段 数据段 代码段 堆栈 指针 vs 引用
原文:http://blog.csdn.net/godspirits/article/details/2953721 BSS段 数据段 代码段 堆栈 (转+) 声明:大部分来自于维基百科,自由的百科全 ...
- GNU make 规则
clean : rm *.tmp 规则格式: targets : prerequisites recipe ... targets : prerequisites : recipe recipe .. ...
- [原创]PostgreSQL Plus Advanced Server监控工具PEM(三)
三.使用PEM Client 在PEM Client中连接PEM Server 点击上图所示的按钮或点击菜单-> 第一次连接PEM Server,会有一次导入证书的操作,点击Yes按钮. 接下来 ...
- Bing Speech Recognition 标记
Bing Speech Services Bing Bing Speech Services provide speech capabilities for Windows and Windows ...
- iOS学习之C语言函数指针
通过函数名调用函数: int max = maxValue(4, 5); printf("max = %d\n", max); 函数类型:int (int, int) 1. ...