目录
前言
这节课我们来介绍下rust中最重要的一个点:所有权系统。这是网上经常说rust无gc的秘密所在。在开始之前,我们来想想JVM系语言,在做垃圾回收的过程,1.怎么找到垃圾,或者说怎么判断该内存上放的是垃圾。2.什么时候清理垃圾。3.怎么清除垃圾。JVM上不同的垃圾回收器,不同的垃圾回收算法都是围绕着这三个问题展开。我们假设一个场景:中学班级毕业,所有毕业的同学需要写完毕业手册后才能离开教室,离开教室后可以清理该同学的东西了。怎么做呢?当同学离开教室的时候,这个同学的东西就可以被清理了。什么叫做离开教室呢?我们都知道代码是有作用域的,说的简单就是那对{},当某个变量离开作用域后,这个变量如果是某块内存的所有者,那么这块内存就可以被释放了,是不是很有趣的思路,其实在c/c++中,内存的释放是程序员自己写的,rust中的内存释放是靠所有权系统保证的,在编译阶段rust编译器会根据所有权系统的规则对代码进行检查,保证内存都能正确的申请和释放,如果不满足规则,编译就不允许通过,这也是很多人说rust不能一次编译,但是一旦编译通过,就是安全可靠的程序。这节课我们来看看rust中所有权系统吧,提前预告涉及的概念多且陌生,耐心看完。
听起来rust中的内存回收是不是挺简单的,只要同学离开教室就可以清理该同学的东西了,假设某本字典是A同学和B同学一起买的,当A同学离开的时候,把字典清理了,这时候B同学想查字典就会造成空指针,这就是悬垂引用,当B同学离开教室时,想要再扔一次字典,此时字典已经被扔过一次,这就会造成内存二次释放。这2个问题都是比较严重的问题。
涉及概念
所有权系统ownership system
所有权ownership
借用borrowing
生命周期lifetimes
所有权机制的核心
1.每一块内存空间都有一个所有者,该所有者对于该内存有读写权限和释放内存的权限,我们将这个所有者称为主人
2.每一块内存空间在任意时刻都只有一个主人,不能同时存在2个主人,比如上面例子中的字典,字典只能有一个主人,但是A同学可以把字典给同学B,这样主人就从A转移给B,其他同学可以借字典使用,但是B才是主人,哪些不是主人的同学没有释放内存的权限,所以离开教室时不会丢字典
3.当主人离开作用域时,该主人释放他的内存区域
再谈变量绑定
再一次看看变量绑定
fn main() {
let x = "hello".to_string();
}
知道为什么在其他语言中这样的代码叫做变量x的声明和初始化,但是在rust中这叫做变量绑定了吧,将字符串绑定到变量x上,从此之后,x就是hello所在内存的主人了,当x走出了作用域之后,hello所在的内存就要被清理。
主人变更-所有权转移
我们说过主人是可以变更的,那么怎么变更呢,通过赋值,下面的代码hello的主人第一次是x,在y = x后,hello的主人已经是y了,这种行为叫做移动move,也叫做所有权转移,但是这里要注意一下,有些值是移动,有些值是复制,比如一些基本类型,例如数字,当操作y=x时,是复制了一个值出来,假设x=7,当操作y=x时,此时内存中有2个7,他们的主人分别是x和y,但是像字符串这样的,才是移动。
重要的3句话:
1.rust中有2种语义:复制语义和移动语义
2.复制语义不会导致所有权转移,移动语义会导致所有权变更
3.实现了Copy trait的都是复制语义,没有实现Copy trait的都是移动语义
所以很简单了,根据上面的三句话,基本类型实现了Copy,所有权不会转移,String没有实现Copy,所以会转移,对于元组,如果元组中的所有值都实现了Copy,那么元组也实现了Copy,不会转移。我贴了一个例子在后面。
fn main() {
let x = "hello".to_string();
{
let y = x;
}
}
看看这个元组的所有权例子,t1的所有元组都实现了Copy,所以t1也实现了Copy,于是在t2 = t1时,会将这个元组复制一份,t1 和 t2分别是2块不同内存的主人,因为是主人,所以我们可以访问t1的元素。t3由于存在一个String类型的元素,String是移动语义,所以t3也是移动语义,那么在t4=t3时,该元组的内存已经属于了t4,不再是t3的了,所有权发生了转移,那么当我们继续使用t3.1时,会收到报错,下面贴了报错的图,可以看到rust编译器解释的很清楚,t3因为没有实现Copy,所以它被移动了,移动了之后,我们就不能使用t3去读取元组了,因为t3不是主人了,不是主人就没有读写权限了。
fn main() {
// 元组的所有元素都实现了copy,元组就实现了copy
let t1 = (1, 2, true);
let t2 = t1; // 所有权不会转移
println!("t1.0 = {}", t1.0);
// 元组存在元素没有实现copy,元组没有实现copy
let t3 = (String::from("hello"), 1, 2, 3);
let t4 = t3; // 这里所有权转移给t4, t3不再是主人
println!("t3.1 = {}", t3.1);
}
那么结构体呢?结构体会发生所有权转移么?我们来试一试,实践出真知。
代码如下,其实对于结构体来说,不管结构体中的元素有没有实现copy,结构体默认都是移动语义,所有权会发生移动。但是大家会问,这不合理啊,如果结构体中的元素都是copy语义,结构体其实也可以是copy语义啊,是的,没错。所以说结构体默认是移动语义,如果结构体的元素都是copy语义,可以在结构体顶部加上#[derive(Copy,Clone)]后,结构体就是copy语义了,但是如果结构体中存在移动语义的元素,即使加了这个,结构依然是移动语义,关于这个derive属性,可以百度了解一下哦。
fn main() {
let s1 = Student1{
age: 23,
};
let s2 = s1;
println!("s1.age = {}", s1.age);
let s3 = Student2 {
name: String::from("hello"),
};
let s4 = s3;
println!("s3.name = {}", s3.name);
}
struct Student1 {
age: i32,
}
struct Student2 {
name: String,
}
总结
这节课是rust无gc的核心秘密所在,也是和其他编程语言非常不同的地方,rust的所有权保证了每一块内存都有自己唯一的主人,当主人离开作用域后,内存可以释放,从而rust的性能可以堪比c/c++这些需要手动申请释放内存的语言,rust程序需要的内存量比起java来说也少了很多倍,原因就在这里了。这一节课值得好好研究,也可以在网上看些其他材料综合理解,同时也欢迎大家评论交流哈。