Rust之路(4)——所有权

【未经书面同意,严禁转载】 -- 2020-10-14 --

 

所有权是Rust的重中之重(这口气咋像高中数学老师 WTF......)。

所有权是指的对内存实际存储的数据的访问权(包括读取和修改),在大多数语言中,一个数据赋值给一个变量,这个变量就拥有了数据的访问权。然后可以定义很多引用指向这个变量和这段数据,所有的引用都可以修改它。但在Rust中刷新了这种观念:读数据可以共享,但写数据必须是独占的!这样就能保证数据安全性,以及并发过程中的数据竞争。Rust用所有权来实现这一切。

所有权和另外两个概念借用和生命周期是相关的,可以说是一体的。借用其实就是前面讲的引用干的事情。大多数Rust书籍——包括官网的《The Book》——都是按章节一 一介绍,总感觉不那么立体,理解起来也慢。

  • 引用(借用):borrowing
  • 所有权:ownership
  • 生命周期:lifetimes

搞定三者应该是一揽子工程。

经闭关苦思冥想,我总结了一个形象的比喻,和读者分享。

每个人都有一张身份证,来记录个人的唯一信息身份证号,以及姓名:张三、性别:男 等个人信息,你可以以这个身份拥有房产、汽车、存款等财物(相当于内存中的实际数据),并可以修改房产的布局、启动汽车、存取款。这些财物属于这个身份的,这个身份拥有这些财物,这个身份对财物拥有所有权。这个时候,任何其他人无权进你的房间、汽车、私自查你的账户。

如果李四向张三打了招呼:我想去你房子里看看,张三允许了李四去参观房子、查看账户,但此时未经张三的书面允许还不能去搬动或迁移财物。借用者只能使用,不能改变,这是只读引用,也叫共享引用,因为可以有多人同时使用,这时候王五、赵六都可以同时去查看张三的财物信息。

然而如果有胡七取得了张三的身份证,套用了他的身份,就可以通过这个身份转移财产、修改财物的属性。但Rust为了避免多人同时修改数据,除了胡七能修改财物,这时候连张三也是个木头人了,自己都无权去改变自己的财物。而胡七知道自己随时要修改财物,也不允许任何其他人来查看,以防看到的信息随后就被改变而失效。这种情况一直延续到胡七放弃这个身份,财物才返还给张三。

另外,张三可以把自己的财物转让给别人周八,这时候张三的身份下就没有财物了。如果再想通过张三获取财物信息,就会出错。注意和以上借用情况的区别,前面是财物的所有权一直归张三所有,而此处,是所有权已经到了周八手中。

------以上比喻仅限于此处所述情节,至于现实中的诸多无关因素请忽略------

这就所有权的概念,内存上的数据就像财物,只能有一个人真正拥有(现实生活中就算两个人拥有一套房,也得按比例分配),财物可以转让,共享信息,也可以借出去。对借用者来说,可以借来一个观看权,这个权并不是所有权,也可以借来所有权,这时候财物是其他人不能访问的。

此外,再加上时间这个维度!

张三从出生到挂了,是他的一个生命周期。出生后的某个时间点拥有了财物,相当于给张三的身份做了值初始化,有了实际的数据。如果有人借用他的身份,无论是只读引用(李四)还是可变引用(胡七),都必须在张三有了数据之后才能借用。张三在挂的时候,他名下的所有的财物都要销毁,所以在张三消失之前,李四胡七等人要先把借用销账(要么改而指向其他活着的人,要么自杀)。

张三的财物被周八拥有后,张三就恢复了两袖清风的状态,相当于变量又变为未初始化状态。Rust中,未初始化的变量只能赋新值,是不能直接使用的。

 

一、
let mut Zhang_San = "1套房产".to_string();
//创建变量Zhang_San,并用字符串初始化,这个字符串的所有者是Zhang_San
//------------------------------------------------------------
二、
let mut Zhang_San = "1套房产".to_string(); Zhang_San
+= "1台车";
//Zhang_San声明的时候用了mut关键字,可以修改这个字符串 //------------------------------------------------------------
三、
let mut Zhang_San = "1套房产".to_string(); Zhang_San += "1台车";
let Li_Si
= &Zhang_San;
//这时候,Li_Si用只读方式借用了Zhang_San,Zhang_San和Li_Si都可以访问字
符串,但都不能更改,数据的所有权还是归Zhang_San
//------------------------------------------------------------
四、
let mut Zhang_San = "1套房产".to_string(); Zhang_San += "1台车"; let Li_Si = &Zhang_San;
let Wang_Wu
= &mut Zhang_San;
//创建了可变引用后,所有其他的引用全部失效,如果后面代码再使用Li_Si,程序会
出错。此时Zhang_San拥有所有权,但是不能使用,只能使用Wang_Wu访问和修改

//------------------------------------------------------------
五、
let mut Zhang_San = "1套房产".to_string(); Zhang_San += "1台车"; let Li_Si = &Zhang_San;
let Wang_Wu = &mut Zhang_San;

let Hu_Qi
= Zhang_San;
//Hu_Qi把Zhang_San的所有权拿走,现在Zhang_San以及所有Zhang_San的引用全
部不能再用。这叫做所有权的移动(Move)。

需要注意的是:如果Zhang_San的类型不是字符串,而是整型、布尔等基本类型(基本类型的含义依然是数据类型上篇开头的含义),第五段代码的运行就会不完全一样。Hu_Qi通过赋值运算符(=)取得的,不是Zhang_San的所有权和数据,而是一份拷贝,Hu_Qi拥有这份拷贝数据的所有权。这是因为基本类型都实现了Copy trait,在赋值的时候,会隐式调用拷贝行为,源数据继续存在。

由于所有权的概念,把向量中某个元素赋值给其他变量是不行的。

let mut v = vec![100, 101, 102, 103];
let third = v[2]; //错误!

 

因为向量是一个整体,如果某个元素的所有权被移走,向量就像被打掉了一个门牙。。。漏风了。不雅观也不健康。因此,这种情况,应该用引用(借用):let third = &v[2],如果需要改动这个值,则使用可变引用:let third = &mut v[2]。

需要注意的还有在循环语句中,要禁止变量的移动,而是用引用替代:

let x = vec![10, 20, 30];
while f() {
  g(x); // 错误!在循环第一次后,x就失去所有权变为未初始化变量了
}

// 然而下面这种方式就可以编译,每次失去所有权后,重新给它一个值,又初始化了
let mut x = vec![10, 20, 30];
while f() {
  g(x);           // 移走x
  x = h();        // x重新赋值,下次循环又可以消费x了
} 

 

所有权树

每个变量拥有一个值,如果这值是复杂类型,它又可以拥有其他值,例如:

let v = vec![1, 2, 3];
let arr = ['A', 'B'];
let t = (v, arr);

 

t 拥有一个元组的所有权,这个元组手握一个向量和一个数组的所有权,而其中的向量有三个整型的所有权,数组由两个字符类型的所有权。

所有者 t 和拥有的值形成了树。在所有权树的最终根是一个变量t;当这个变量超出范围时,整个树也跟着它销毁。Rust的单一所有者规则禁止网状连接结构,程序中的每个值都是某个树的成员,根在某个变量上。

Rust程序通常不显式地删除值,C和C++程序使用free和delete销毁一个堆数据的变量。在Rust中删除一个值的方法是以某种方式从所有权树中删除它:通过离开变量的作用域(见下文),或者从向量中删除元素,或者其他类似的行为。关键是,Rust确保某个值会连带它拥有的值一起销毁。

 

作用域、生命周期

在Rust的代码中,一个大括号{...}就是一个代码块(block)。在流程控制语句中,通常会有若干个代码块,例如if Condition {...}、while Condition {...},或loop {...}等等,在这些语句中,代码块用于组织代码,完成一个判断或循环功能。除此之外,代码块还起到区域隔离的作用:一个代码块看做一块区域,与另外的区域相互隔离,两个代码没有重叠的区域之间的变量和函数名不会冲突,区域中的变量都只在自己的区域内起到作用,因此代码块也可以叫做作用域。代码块可以嵌套,作用域也是嵌套的。当然一个源代码文件也是一个作用域,一个程序所有的代码文件是一个更大级别的作用域。

在一个作用域内声明的普通变量,仅存活于本作用域,代码运行到作用域结束,这个变量随即被销毁。从变量声明到此,就是这个变量的生命周期。根据Rust的安全原则,变量销毁的时候,归它所有的数据以及这些数据所有的数据,都会销毁。这样就不会有野指针(一般参考书都翻译成悬空指针)。

除了流程控制语句,也可以把任意一些代码用大括号括起来(不造成代码混乱的前提下),变为一个作用域,这样可以有效地控制变量的生存与毁灭!

对于一个值a,如果它有一个引用 r_a,那么 r_a必须在a进行初始化后指向它,在a销毁以前销毁(或同时到达作用域的末尾)。

 

RC和Arc

在Rust中,通常一个值只有一个所有者,一个所有者可以拥有多个值,当所有者超出作用域销毁后,值也被销毁。这样在某些情况下稍显死板,所以出现了一种引用计数型指针Rc,可以延长数据的生命。这有点像Java或C#中的垃圾回收机制,某个值可以有多个Rc指针指向它,只要是有一个这样的指针存在,这个值就不会销毁,直到所有指针都销毁了,这个值才被销毁。

use std::rc::Rc;   //use 是引入rc模块的RC类型,就像C#的using和python的import

// Rc指针可以包装任何类型的值,通过clone()方法复制出多个同目标值的Rc指针
let s: Rc<String> = Rc::new("山神庙".to_string());   //创建Rc指针
let t: Rc<String> = s.clone();
let u: Rc<String> = s.clone();

 

Rc指针可以自动解引用,调用所包装的类型的方法,此例中可以直接在Rc<String>类型的s、t、u上使用String的任何常用方法。

因为Rc是共享指针(多个指针共享一份数据),所以是不可变类型,一经创建就不能通过它修改所包装的值,也不可以把指针指向一个新值。

use std::rc::Rc; 

let s: Rc<String> = Rc::new("白衣秀士王伦".to_string());   //创建Rc指针
s.push('气');   //错误,Rc指针的目标值不可变
s = Rc::new("托塔天王晁盖".to_string()); //错误,不允许重新赋值

关于所有权,就先介绍这么多。

再次强调一下,对于初学者,通常把所有权随意转移,也通常不注意引用的生命周期必须必目标值的生命周期短,尤其是在函数调用中。

要养成先规划各变量的生命周期的习惯,学Rust不仅学语法,还提高了程序规划的能力。。。。。。

所有权这个概念,真是难者不会,会者不难,关键是要理解透彻。也许我的理解某些地方也有偏差,在以后深入学习中,不断进步吧!

posted @ 2020-10-14 21:56  sumyuan  阅读(424)  评论(0编辑  收藏  举报