• Ownership
  • References and Borrowing
  • Lifetime

1. Ownership

rust的ownership系统是它区别与其它语言的最主要的特征。只有理解了ownership系统,才能真正算是入门。

Rust的绑定变量有一个属性:获得它所绑定资源的所有权。这意味着当绑定变量超出作用域时,它所绑定资源的资源就会释放。

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

绑定变量v的作用域是函数foo的函数体,创建v时,先会在栈上分配空间来保存v这个变量 ,然后会在堆上分配空间以保存它的3个元素。当v超出作用域时,Rust会清除栈和堆上这些资源。

有一点要注意:Rust确保有且只有一个变量绑定到给定的资源

let v = vec![1, 2, 3];  //创建一个vector,并绑定到一个变量let v2 = v;   //把它赋给另一个变量。println!("v[0] is: {}", v[0]);   //使用原来的那个绑定变量。

运行上面的代码会报错。

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

这行代码是把v赋给v2,它们都指向同一个vector,这违反了Rust安全承诺。所以在这个赋值后Rust不允许再使用变量v。在编译器优化时,可以会把它释放掉。看起来就像v的所有都转移(move)到v2了。

下面我们再看一个例子,这回我们把类型从vector换成i32.

let v  = 1;let v2 = v;println!("v is: {}", v);

这个代码就可以运行,为什么?因为这个例子里v的类型是i32,它实现了Copy trait,所以let v2 = v;这行代码执行时,rust会把v的值深度copy一份,然后给v2,所以v在赋值后可以用的。
v2和v拥有不同的资源,分别是各自资源的owner。

move还是copy ?

当一个局部变量用做右值时,它可能会被move或copy,取决于它的类型,如果它实现了Copy这个trait,那它就会被copied,否则就会被moved.

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

vector没有实现Copy trait,所以在赋值后v就不可以用了。

如果我们写了一个函数,以vector为参数,为了能让函数调用后原来的变量能正常使用,我们必须手动归还这个ownership。

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); //调用并归还

这简直太变态,无法接受啊!

所以rust引入了borrowing 来解决这个问题。

2. References and Borrowing

在Ownership一节,我们给出了一个手动归还Ownership例子,手动归还实在太不方便。

Rust使用reference 来解决这个问题。这是reference版本的。

fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {// do stuff with v1 and v2// return the answer42
}let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];let answer = foo(&v1, &v2);// we can use v1 and v2 here!

reference是什么?官方文档是这样解释的。

We call the &T type a ‘reference’, and rather than owning the resource, it borrows ownership.

borrow,借,也就是所有权是没变的。我借你的书看,书还是你的(所有权归你),但是我现在在用它。
引用也是这个意思,引用可以使用资源,但是不拥有所有权。

默认的References不可变的,跟绑定一样.

fn foo(v: &Vec<i32>) {v.push(5);
}let v = vec![];foo(&v);

会报错:

error: cannot borrow immutable borrowed content `*v` as mutable
v.push(5);
^

不可变的引用,不能修改资源的内容。如果要修改资源的内容,我们先取得可变引用

let mut x = 5;
{let y = &mut x;*y += 1;
}
println!("{}", x);

x的值被修改了。你会奇怪,我们为什么要把修改的代码放在{}块里。如果我们把这两个花括号去掉会报错。

error: cannot borrow `x` as immutable because it is also borrowed as mutableprintln!("{}", x);^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow endslet y = &mut x;^
note: previous borrow ends here
fn main() {}

为什么?
我们先来说说Rust对references规定吧。

  1. 所有的引用的作用域必须小于所有者(owner)的作用域。
  2. 你可以有多个不可变的引用(&T),但
  3. 同时只能有一个可变的引用(&mut T)

我们再来看上边的例子:

let mut x = 5;let y = &mut x;    // -+ 可变引用 y 开始生效//  |
*y += 1;           //  |//  |
println!("{}", x); // -+ - 试图使用原来的可变绑定// -+ 可变引用 y 离开作用域

我们无法在可变引用y的作用域里使用x. 因为它违反了同时只能有一个可变引用的这条规则。

了解了引用我们下面再来学习Liftetime.

3. Lifetime

Lifetime是刚接触rust时特别容易产生迷惑的一个概念,所以我在这里花了比较大的篇章来说明它,基于我自己的理解,希望能给大家讲明白。

在上一节里我们讲了引用和借用
把资源的引用借给他人使用其结果可能会很复杂。假如:

  1. 我有一个资源
  2. 我把这个资源的引用借给你
  3. 我释放这个资源(我不关心你那儿还有一个引用,因为我是这个资源的owner,我有权释放它)。然后
  4. 你要使用这个引用。

第4步,当你使用引用时,它所指向的资源已经不在了!这将导致不可预知的问题。

如何避免上述情况的发生?

一种解决方案就是: 当还有一个指向资源的引用存在时,资源就不能被释放。 没有指向资源的引用时,才能释放它。

这个方案下第3步就不会释放资源,第4步就是安全的。

在这种情况下资源怎么释放呢?有两种方案,引用计数(ARC)和垃圾回收器GC. Objective-C和Swift使用的是ARC,java和.net使用的是GC.

rust没有使用ARC也没有使用GC. onwer使用完了资源(通常是owner超出作用范围自动销毁)就会释放资源。

那第4步怎么办?

Rust使用某种机制来保证第4步不会发生!

你会说怎么不会发生? 我代码就要这样写,那当然会发生啊,比如:

struct Foo {f:Box<i32>,
}fn main() {let y : &Foo;{let mut x = Foo{f : Box::new(18)};   // 这相当于第1步,获得资源。y = &x;                              // 这相当于第2步, 借出 reference.}                                        // 这相当于第3步 onwer超出作用范围,释放资源println!("{}",y.f);                      // 这相当于第4步。 通过reference来使用资源。}

好像第4步发生了呀。 不好意思,这段代码无法成功编译。Rust compiler会检查引用的lifetime和
owner的lifetime。它发现引用的生命期比资源的owner的长时,它编译时就报错了,根本就不会到
运行这一步。所以第4步是不会发生的。

因此在Rust里,你必须在owner释放资源之前使用它(借出)的引用。就也是上述的第4步应该发生在第3步之前。

1 我有一个资源

2 我把这个资源的引用借给你

4 你使用这个引用。

3 我释放这个资源。

上述的代码如果用java或swift来实现肯定可以编译通过。 我们已经习惯写这样的代码了,我们理所当然
地认为这样的代码可以运行。但是在rust里,你不能这样写代码,因为rust不允许你这样写。

如何保证第4步发生在第3步之前呢?rust是通过保证资源owner活得比它的任何一个引用更长来实现的。

Rust通过叫lifetime的概念来实现。

下面我们举些例子来说明lifetime。在这之前,请记住:有引用才有lifetime,lifetime是跟引用关联的.

struct Foo {f : Box<i32>,
}struct Bar {foo : &Foo,  //struct包含引用,就必须明确指定lifetime.
}fn main() {let mut a = Foo {f: Box::new(14)};let y : &Foo;{let x = &a;y = x;}a.f = Box::new(1);println!("{}" ,  a.f);
}

上面的代码会在第2个struct: Bar 处报错error: missing lifetime specifier。而第1个struct Foo就不报错这个错误,因为它没有用包含引用。

好,我们现在给它加上lifetime。

struct Foo {f : Box<i32>,
}struct Bar<'a> {foo : &'a Foo
}fn main() {let mut a = Foo {f: Box::new(14)};let y : &Foo;{let x = &a;y = x;}a.f = Box::new(1);println!("{}" ,  a.f);
}

OK,可以编译通过了。

很多人会被Bar<'a>foo : &'a Foo里面的'a搞懵了,不明白它是干什么的,它如何起作用.

'aNamed lifetime,中文可译为带名生命期。它实际上是告诉编译器,struct Bar有引用,这个引用的lifetime我们把它命名为:a.

做为开发人员我们不用关心'a是怎么生效的。因为我们不会直接用到它。开发人员要就做的就是给引用加上一个带名生命期。然后由编译器来使用它。

当我们的struct包含了一个引用,那我们struct的实例不能比它包含的引用活得更长

如果不能理解这句话,请翻到前面看开头的那个4步的说明.

那什么是lifetime ? lifetime是rust用来度量引用生命期的一个辅助标识。编译器用它来计算引用的寿命有多长。 很多人认为lifetime这个词选择不太好,容易产生误解[1]。

'a这种东西只是用来计算生命期的标识,好理解了吧?

下面我们看一个struct包含多个引用时的情况,这时带名生命期的作用就更容易理解。

struct Foo {f : Box<i32>,
}struct Bar<'a,'b> {foo : &'a Foo,doo : &'b Foo
}fn main() {let mut a = Foo {f: Box::new(14)};let d : &Foo;{ // block1let mut b = Foo {f: Box::new(13)};let bar = Bar{ foo : &a,doo : &b};println!("{}" ,  bar.foo.f);d = bar.foo;} // end of block1//a.f = Box::new(1);println!("{}" ,  d.f);
}

首先如果一个struct有多个引用,那它的实例的寿命只能和最短命的那个引用一样长。 a的寿命是整个main函数,而b的寿命是block1,
所以bar的寿命只能是block1。

如果我们把block1的最后一行改成:

d = bar.doo;

就会无法编译:

struct Foo {f : Box<i32>,
}struct Bar<'a,'b> {foo : &'a Foo,doo : &'b Foo
}fn main() {let mut a = Foo {f: Box::new(14)};let d : &Foo;{ // block1let mut b = Foo {f: Box::new(13)};let bar = Bar{ foo : &a,doo : &b};println!("{}" ,  bar.foo.f);d = bar.doo;} // end of block1//a.f = Box::new(1);println!("{}" ,  d.f);
}

好,接下来我们看看lifetime和函数的关系。

struct Foo {f : Box<i32>,
}fn test(a : &Foo,b : &Foo) -> &Foo {println!("a : {} - b : {}",a.f,b.f);b;
}fn main() {let  a = Foo {f: Box::new(14)};let  b = Foo {f: Box::new(13)};//println!("{}" ,  bar.foo.f);test(&a,&b);
}

函数test有两个类型为&Foo,无需为这个函数显式指定lifetime。如果函数返回一个引用,则必须为函数显式指定lifetime。

我们修改代码,让函数返回一个引用,我们先不给它加lifetime,看看编译器提示什么.

struct Foo {f : Box<i32>,
}fn test(a : &Foo,b : &Foo) -> &Foo {println!("a : {} - b : {}",a.f,b.f);b;
}fn main() {let  a = Foo {f: Box::new(14)};let  b = Foo {f: Box::new(13)};//println!("{}" ,  bar.foo.f);test(&a,&b);
}
<anon>:5:31: 5:35 error: missing lifetime specifier [E0106]
<anon>:5 fn test(a : &Foo,b : &Foo) -> &Foo {^~~~
<anon>:5:31: 5:35 help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
error: aborting due to previous error
playpen: application terminated with error code 101

我们的函数返回了一个引用,px 有两引用类参数,编译器不知道返回的引用是从哪个参数借来的.所以时我们必须显式指定lifetime.

fn test<'a> (a : &Foo,b : &'a Foo) -> &'a Foo {println!("a : {} - b : {}",a.f,b.f);b
}

可不可以返回值的lifetime与参数的不相关呢?

fn test<'a,'b> (a : &Foo,b : &'a Foo) -> &'b Foo ;

上面的函数可能实现吗?如何从函数里面返回一个带新的lifetime的引用?在函数里新创建一个Foo?这样它就有了一个新的lifetime?
问题是函数体内创建的实例会在函数返回时销毁,引用就会失效.

fn test<'a,'b> (a : &Foo,b : &'a Foo) -> &'b Foo {println!("a : {} - b : {}",a.f,b.f);&Foo{f : Box::new(12)}
}

可以看出来函数返回值的lifetime一定是跟某个参数的一致的

函数返回一个引用时必须显示指定lifetime,因为这个返回的引用延长了引用生命期.

没有返回值的函数也能延长引用的生命期.

struct Foo {f : Box<i32>,
}struct Link<'a> {link: &'a Foo,
}fn store_foo<'a> (x: &mut Link<'a>, y: &'a Foo) {x.link = y;
}fn main() {let a = Foo{f : Box::new(1)};let x = &mut Link{ link : &a };if false {let b = Foo { f: Box::new(2) };store_foo(x, &b);  //在这里试图延长b的引用的生命期,但是b会在if块后销毁,&b就会成为野引用,所以这行代码无法编译.}
}

Rust的Borrow和Lifetime虽然有一点难理解,但请相信,一旦弄懂并开始coding,你会爱上它,:D。

Rust语言Ownership,Reference和Lifetime详解相关推荐

  1. R语言可视化绘图基础知识详解

    R语言可视化绘图基础知识详解 图形参数:字体.坐标.颜色.标签等: 图像符号和线条: 文本属性: 图像尺寸及边界: 坐标轴.图例自定义等: 图像的组合: #install.packages(c(&qu ...

  2. php函数find的用法,c语言find函数的用法详解

    c语言find函数的用法详解 C语言之find()函数 find函数用于查找数组中的某一个指定元素的位置. 比如:有一个数组[0, 0, 5, 4, 4]: 问:元素5的在什么位置,find函数 返回 ...

  3. java语言链栈_Java语言实现数据结构栈代码详解

    近来复习数据结构,自己动手实现了栈.栈是一种限制插入和删除只能在一个位置上的表.最基本的操作是进栈和出栈,因此,又被叫作"先进后出"表. 首先了解下栈的概念: 栈是限定仅在表头进行 ...

  4. 大二c语言期末考试题库及详解答案,大学C语言期末考试练习题(带详解答案)...

    <大学C语言期末考试练习题(带详解答案)>由会员分享,可在线阅读,更多相关<大学C语言期末考试练习题(带详解答案)(55页珍藏版)>请在金锄头文库上搜索. 1.一. 单项选择题 ...

  5. c语言线性表库函数大全,数据结构(C语言版)-线性表习题详解

    <数据结构(C语言版)-线性表习题详解>由会员分享,可在线阅读,更多相关<数据结构(C语言版)-线性表习题详解(23页珍藏版)>请在人人文库网上搜索. 1.数 据 结 构 ,线 ...

  6. c语言 read 文件字节没超过数组大小时会怎样_剑指信奥 | C 语言之信奥试题详解(四)...

    趣乐博思剑指信奥系列 ❝ 趣乐博思剑指信奥系列,专门针对全国青少年信息学奥林匹克联赛 NOIP 而开展的专业教育方案.开设的课程有 C 语言基础,C++ 语言基础,算法设计入门与进阶,经典试题分析与详 ...

  7. Go 语言 bytes.Buffer 源码详解之1

    转载地址:Go 语言 bytes.Buffer 源码详解之1 - lifelmy的博客 前言 前面一篇文章 Go语言 strings.Reader 源码详解,我们对 strings 包中的 Reade ...

  8. C语言中指针与数组的区别,C语言 指针与数组的详解及区别

    C语言 指针与数组的详解及对比 通俗理解数组指针和指针数组 数组指针: eg:int( *arr)[10]; 数组指针通俗理解就是这个数组作为指针,指向某一个变量. 指针数组: eg:int*arr[ ...

  9. gets和fgets函数及其区别,C语言gets和fgets函数详解

    gets和fgets函数及其区别,C语言gets和fgets函数详解 每当讨论 gets 函数时,大家不由自主地就会想起 1988 年的"互联网蠕虫",它在 UNIX 操作系统的 ...

最新文章

  1. 网易云摸到了大象灵巧的鼻子
  2. 阿里云服务器ssh连接经常断开
  3. php mysql随机记录_php随机取mysql记录方法小结
  4. 【面试题3】二维数组中的查找
  5. Linux中要重启apache服务与在windows是有很大的区别,下面我们来介绍一下
  6. [sdoi2015]排序(搜索+剪枝优化)
  7. HTML5游戏开发5条建议及开发工具分享
  8. 用微PE安装KALI LINUX到U盘,【U盘安装kali】U盘+kali+pe三合一教程!装机,存储(自己用来做U盘使用的空间)...
  9. HTML网页设计制作大作业 - 绿色环境保护HTML5网站模板(4个页面)
  10. 浅谈App Hybrid混合开发的五种方案
  11. 我的世界服务器哪个有自动铺路,我的世界自动铺路指令是什么
  12. 数据分析师15-面试全流程
  13. 电商商品爬虫,亚马逊amazon采集源码
  14. Android读写日历,Android日历提醒问题总结
  15. dvi线支持多少分辨率_dvi接口有哪几种_dvi支持最大分辨率
  16. 34岁程序员面试美团被拒绝:只招30岁以下,卖力能加班工资又少的
  17. oracle sql 时间差
  18. 华为云fusionsphere 6.1组件功能
  19. 《杰克.韦尔奇自传》读书笔记
  20. java 动物声音模拟器_动物声音模拟器软件

热门文章

  1. 音视频的流程:录制、播放、编码解码、上传下载等
  2. QQ浏览器X5内核问题汇总
  3. QQ第三方登录-QQ互联开发者申请的坑(个人接入,时间:2019-6-3)
  4. oracle招聘ocp认证,OracleOCP认证要通过哪些考试?
  5. 初识Hibernate——关系映射
  6. hdu_1429 胜利大逃亡(续)
  7. handler原子锁_spring-boot-starter-current-limiting
  8. 《RAFT-Stereo:Multilevel Recurrent Field Transforms for Stereo Matching》论文笔记
  9. 《点燃我,温暖你》爱心代码复现
  10. 双操作系统安装(五)Windows及Manjaro Linux双系统安装教程