Bootstrap

【第五课】Rust所有权系统(一)

目录

前言

所有权机制的核心

再谈变量绑定

主人变更-所有权转移

总结


前言

这节课我们来介绍下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来说也少了很多倍,原因就在这里了。这一节课值得好好研究,也可以在网上看些其他材料综合理解,同时也欢迎大家评论交流哈。

;