北川广海の梦

北川广海の梦

浅谈Rust内存的所有权

347
2020-08-23

内存安全

程序其实就是逻辑代码和数据结构的结合体,而能被代码操作的数据一定是保存在内存中的,所以内存中的数据安全是非常重要的。这里的安全,主要是指保证数据的正确性,尽量减少各种情况下的数据被污损,以及保证内存存储空间的有效利用,减少甚至完全避免无效的内存分配。

本人编程从C语言入门的,学习了基本语法,写过简单的数据结构。最印象深刻的在于用到了malloc分配的变量,需要对其进行free释放内存。当时并不理解,并且几乎从来就没记起要写。后来学习了托管型语言,.NET和Java,对于这两门语言来说,只需要专注于程序的逻辑,不用考虑内存的回收,因为存在着虚拟机。但是它们在多线程情况下,仍然有着非常高的数据污染的危险(这是程序员导致的,语言本身没有任何问题)。

后来了解到了一门叫做Rust语言,它没有运行时,但是却能很好的避免内存泄露问题,并且通过编译器,极大程度上保证了内存数据安全,顿时对其产生了兴趣。

Rust如何即使释放内存

所谓内存泄漏,即所分配的内存没有得到释放或者没有被及时的释放。内存泄漏可是严重性比较高的问题,试想一个高访问量的服务器程序,存在内存泄漏,那么估计部署上去没多久,服务器的内存就被消耗光了,直接导致服务挂掉。
虽然Java、.NET等虚拟机平台提供了GC,但是垃圾回收本身存在着较大的性能消耗,并且在某些GC,是会暂停所有线程工作的,这对于某些要求的应用场景,是非常致命的。

Rust为了解决这一问题,引入了一套所有权机制。变量(我还是习惯说变量)有它们所绑定的的值的所有权。这意味着当一个绑定离开作用域,它们绑定的资源就会被释放。

其中作用域用“{}”表示
那么很显然,一个函数表示一个作用域,一个if也会带有一个作用域,甚至我们可以自己手动设定一个作用域。

下面是一个最简单的例子:

fn main() {
    {
        let foo = String::from("Hello");
        println!("{}", foo);
    }
}

foo由两部分组成,一部分在栈上,存储的是一个指针,指向了堆中实际存储“Hello”这个字符串的地方。
在作用域结束后,栈上和堆上的内存都会被回收。
而如果是Java程序的话,需要等到这个字符串没有可达性的时候,才会被标记为需要回收的对象,并且在下一次GC时被回收,具体回收时间是未知的。

所有权:

下面这个例子会在编译时报错

fn main() {
    let foo = String::from("Hello");
    func(foo);
    println!("{}", foo); //value borrowed here after move
}

fn func(foo: String) {}

因为在进行func函数调用的时候,foo对象已经赋值给了func的形参“foo”,这里就产生了一个move操作,这个move我将其称之为转移。

其实上面的例子和下面是一样的:

fn main() {
    let foo = String::from("Hello");
    let foo1 = foo;
    println!("{}", foo); //value borrowed here after move
}

因为众所周知,函数的传参一定是拷贝传参,就相当于有一个命名为函数形参的变量,被所传的参数赋值了。

而Rust中有一个独特的机制,那就是堆中的对象在任何时候只能和一个对象绑定。
如果说其他语言堆与栈的关系就如同一个人可以有许多电话号码,通过每一个号码都可以打给这个人,而Rust就相当于一个账号,只能有一个密码,如果密码进行了修改,那么原来的密码一定会失效
所以foo在赋值给foo1的时候,进行了所有权转移,那么原来的foo自然失效了,那么现在只有foo1有效。

上面讲了在堆上分配内存的对象的情况,那么栈上分配的对象是怎么样的呢?
下面再看一个例子

fn main() {
    let foo = 12;
    let foo1 = foo;
    println!("{}", foo); 
}

上面的程序跑起来不会有任何问题。原因非常简单,因为i32并不是在堆上分配内存的,即使佛foo1被foo赋值了,foo还是foo,因为此时为拷贝,而非拷贝地址

明天会继续写关于Rust的引用。