Rust_0基础学习笔记_02:所有权机制2

我们接着上一篇笔记讨论,也就是所有权机制的第三大支柱:Move机制的相关内容。

实际上,Move机制并不复杂,连笔者这样的0基础人士都能比较轻松地理解。本篇主要是做一个归纳整理。

在进入正题之前,我们先来看一下Rust的所有权机制的具体规则:

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped. -- Rust Book

以上是官方给出的定义,演译一下:Rust程序中的每一个值(也就是堆上的一块内存),都有一个所有者。既然是所有者,它就有对这块内存进行任意读和写的权利。这个权利和我们现实生活对于动产/不动产的所有权是一样的,都是排他的权利。因此,同一时间,每一个值只能有一个所有者。一旦所有者离开了其作用域,由其所有的值就会被drop掉(按照上一篇笔记的语境,就是脱钩)

规则很好理解,但笔者想要讨论的重点是:Rust是如何保障这个规则得到执行的。实际上,上述规则中的第一条和第三条均属于常规操作。Rust的操作和其他类似语言没有什么区别,还是RAII机制:运行时自动申请分配堆内存,实现挂钩;作用域结束时,自动调用Drop函数,实现脱钩。

剩下的第二条:如何保证同一时间只有一个所有者,即当所有者的作用域结束时,保证只对堆内存释放一次,才是Rust真正的创新之处。这正是Move机制要解决的问题!

继续按照上一篇的思路,从问题出发,我们一步步来拆解Move机制。

“保证同一时间只有一个所有者”,这句话的意思是,如果存在多个潜在的所有者共同声称对某个值享有所有权的时候,只能确认其中一个为真正的所有者。这样理解的话,自然就会有如下的问题蹦出来:

  • 什么样的情景下才会有多个潜在所有者?
  • 确认真正所有者的标准是什么?

第一个问题实际上是Move机制的适用情景。笔者目前学习到的适用情景,包括如下几类:

  1. 将一个已经定义的左值作为右值绑定给新的左值;
  2. 将一个已经定义的左值作为参数传递给函数;
  3. 将一个已经定义的左值作为参数传递给函数并返回;
// 1.将一个已经定义的左值作为右值绑定给新的左值;
let s1 = String::from("Hello World");
let s2 = s1; // 所有权从s1转移至s2

// 2.将一个已经定义的左值作为参数传递给函数;
let s1 = String::from("Hello World");
get_string(s1); // 所有权从s1转移至s

fn get_string(s:String) {
    // do nothing;
}

// 3.将一个已经定义的左值作为参数传递给函数并返回;
let s1 = String::from("Hello World");
let s2 = get_string(s1); // 所有权从s1转移至s再转移至s2

fn get_string(s:String) -> String {
    s
}

实际上,所有情景都可以归类为一种情景:将左值(source)作为右值为新的左值(destination)【赋值】 或者说 【绑定】,因为给函数传递参数,相当于将值绑定给函数的参数。所以,未来学习过程中很有可能还会发现这个基本情景的其他延展。

OK,现在我们知道了,一旦出现上述情景,Move机制就会启动。

第二个问题则是关于Move机制的具体实现

这里,我们得回顾一下内存管理的知识。所谓的所有权,实际上是针对堆内存而言的。也就是说,如果一个变量包括存储在堆内存的数据,这个变量就具备所有者的资格;如果一个变量仅包括存储在栈上的数据,这个变量就不具备所有者的资格。当不具备所有者资格的变量的作用域结束时,并不会触发Move机制。

具体实现的流程,以Move机制最典型的情景为例:

let s1:T = value; 
let s2 = s1;
  1. 当编译进入第二行的时候,对s1(source)进行【资格判断】;
  2. 判断出类型后:
    1. 如果s1具备所有者资格(比如s1是String类型),程序就会强行将其所有权转移给s2(destination),此时的s1就会变成一个未经初始化的变量(即没有被赋值的变量)
    2. 如果s1不具备所有者资格(比如s1是整数类型),程序仅实现s1的栈内存的浅复制,不会转移所有权。这是因为整数型不存在堆内存的值,自然不涉及所有权的问题。

插播一下浅复制和深复制的内容,以加强对这部分的理解。

浅复制(shallow copy):仅复制栈内存的内容。举例,对Rust中的一个String型字符串变量来说,浅复制意味着仅复制栈上的指针、容量和字符串大小,不会涉及堆上的具体字符串内容。

图1:浅复制

深复制(deep copy):则是将栈和堆上的内容一起复制。

图2:深复制

 以上浅复制和深复制的图例均来自Rust Book

我们已经知道了Move机制的运作流程,接下来我们需要更进一步,看看Rust是如何进行【资格判断】和判断之后的处理。

Rust对变量的资格判断,是通过类型/语义判断进行的:

类型/语义所有者资格

类型

实现的Trait

Rust操作图例

i32等,值保存在栈内存

————

值语义

Value Sematic

Copy Trait

在栈上通过Clone方法复制一个新值;

————

不涉及所有权

图1

String等,值保存在栈和堆内存

————

引用语义

Reference Sematic

Drop Trait

将栈上source的值移动至destination(参见图3);

drop source的内存,并让source恢复未初始化状态(uninitialize);

————

转移所有权

图3

即:图1+drop source

对该表需要说明的是:

  1. 类型中的i32、String只是举例,完整的类型建议参照 Rust Book 。本质还是在于类型涉及的内存类型。
  2. Copy TraitDrop Trait,是Move机制在判断出所有者资格后的两种互相独立的处理方法:
    1. Copy Trait
      1. 出现Move机制适用情景时,source不具备所有者资格,不会调用Drop函数;
      2. 通过Clone方法实现浅复制。虽然Rust中Clone方法本身是用来作深复制的,但对于实现Copy Trait的类型,因为只有栈内存的值,所以深复制与浅复制没有区别。
    2. Drop Trait
      1. 出现Move机制适用情景时,source具备所有者资格,会对source自动调用Drop函数;
      2. Rust已经预先确定了某一类型所实现的Trait。对于实现Drop Trait的类型,Rust禁止再实现Copy Trait,即程序员无法通过#[derive(Copy)]的方式为String类型实现Copy Trait。

补充:可以手动实现Copy Trait的类型包括结构体、元组等。如果其字段或元素全部为值语义类型,就可以手动实现将其定义为值语义类型。

图3:Move

从图3我们可以发现,所谓的Move,其实质是如下两步:

  1. 把source浅复制到destination;
  2. uninitialize source

通过这两步,形成了栈上值的移动(所以叫Move)。而从整个机制来看,也就是实现了所有权的转移。

至此,我们就把Move机制的基本内容全部讨论了一遍。最后还有几个补充:

  1. Move机制的这套规则,永远不会自动实现值的深复制(即主动调用Clone方法),只有依靠程序员的显性调用;
  2. 向量和数组等集合,Rust不允许使用a[0]的形式给其他变量赋值,因为这会导致集合的元素被Drop;
  3. for循环中,例如 for i in vec,vec的所有权会被转移给for;
  4. 其他的等后续学习过程中发现了再补充

以上,笔者所认为的所有权机制三大支柱就都讨论完毕了。可以说,关于Rust的所有权机制的核心部分,我们基本上勾勒出了一个总体的认识框架。那么,关于所有权的主要内容还有一个【引用】(或者借用),这部分我们留到下一篇再来讨论。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值