Rust所有权系统

Rust所有权系统

引入所有权

常见的编程语言对内存的管理可以分为两种情况,有GC(garbage collection)或无GC。其中以C#、Go、Java为代表的是有GC的语言,以C和C++为代表的为无GC的语言。

有GC的语言优点是随意申请内存,内存的释放有系统负责。缺点是GC停顿对一切实时性要求较高(如交易)的系统影响较大,GC停顿的时间点是无法精细控制的。

无GC语言的优点是内存的申请和释放可以精细化管理,缺点是可能会造成内存泄漏或因为悬垂指针引起系统重大缺陷。

而Rust引入所有权系统巧妙的解决了这两个问题。简单的说,所有权系统要求在任意时刻,系统内的值(值类型的值或引用类型的值)只能属于一个变量

通常我们认为,Rust语言(其他语言也是)中的值主要有两种类型,值类型Value)和引用类型Reference)。值类型存储在中,引用类型存储在上。一般在传递参数的时候,值类型的值把自身复制一份传过去,而引用类型的值则传递自身的引用(指针)。

在Rust中,我们使用Copy trait来区分值类型或引用类型(trait类似于其他语言中的interface)。如果一种类型实现了Copy trait,那么这种类型的变量在传递参数的时候会复制一份自己传递过去,如果没有实现Copy trait,那么传递参数的时候把自己的引用传递过去。Rust所有权系统要求任意时刻系统内的值只能属于一个变量,所以一个引用类型的变量在传递自身引用的同时失去了值(引用类型的值)的所有权。

let s1 = "hello world".to_string();//String类型未实现copy trait,是reference type
let s2 = s;//此时所用权发生变化,s1持有的hello world转移给s2
println!("{}",s1);//编译错误,因为s1已经不持有hello world了
println!("{}",s2);//编译通过,因为s2持有hello world

再看以下代码:

let i1 = 42;//i32类型实现Copy trait,是value type
let i2 = i1;//i1复制了一份自己的值给i2,此时系统内部栈上存在2个42,i1持有一个,i2持有一个
println!("{}",i1);//编译通过,因为i1持有了一个42
println!("{}",i2);//编译通过,因为i2持有另一个42

能否给一个存在堆上的数据类型实现Copy trait呢?答案是不行的。因为堆上的数据比较大(相对于栈),较难实现按位复制(性能损耗较大)。

是否每一种存储在栈上的数据类型都默认实现了Copy trait呢?也不是,比如struct:

struct Point {
    x:i32,
    y:i32
}
let p1 = Point{x:10,y:20};
let p2 = p1;//在栈中存放的x=10y=20的数据所以权转移给p2
let p3 = p1;//此时p1没有了数据,再赋给p3就会编译出错

当然,我们可以手动添加Copy trait,让Point实现按位复制:

#[derive(Copy,Clone)]
struct Point {
    x:i32,
    y:i32
}
let p1 = Point{x:10,y:20};
let p2 = p1;//编译通过,p1存在栈上的值按位复制了一份给p2
let p3 = p1;//编译通过,p1存在栈上的值按位复制了一份给p3

所有权和生命周期

讲到所有权,不得不将作用域和生命周期。既然没有GC,也不需要手动管理内存,那么Rust是如何释放用户申请的内存的呢?答案就在于Rust的作用域和生命周期。

一个变量的生命周期被严格限制在当前作用域。当碰到}时,当前大括号内申请的变量会被自动释放。当所有权结合生命周期,一些有意思的事情出现了:

fn main() {
    let i = 42;
    let s = "hello world".to_string();
    {
        i;
        s;
    }
    println!("{}",i);//编译通过,在内部的大括号内传递的是i的副本,当离开作用域,副本被销毁,但i还是存在的
    println!("{}",s);//编译失败,传递的是s的引用,外面的s失去了s内部“hello world”的所有权,离开作用域s被销毁
}

以下代码和以上代码原理相同:

fn print_i32(i:i32) {
	println!("{}",i);
}

fn print_string(s:String) {
	println!("{}",s);
}

fn main() {
	let i = 42;
	let s = "hello world".to_string();
	print_i32(i);
	print_string(s);
     println!("{}",i);//编译通过
     println!("{}",s);//编译失败
}

不仅仅函数内部的大括号和函数参数传递能够改变作用域,matchif letwhile let闭包都能改变作用域。

所有权借用

所有权借用对于值类型来说,不复制自身,而是传递自身的引用(指针),对于引用类型来说,不是把所有权出让出去,而是依旧持有所有权,只是借给对方用一下。出借所有权必须显示的使用取地址符&

fn print_i32(i:&i32) {
	println!("{}",i);//自动解引用,相当于println!("{}",*i);
}

fn print_string(s:&String) {
	println!("{}",s);//自动解引用,相当于println!("{}",*s);
}

fn main() {
	let i = 42;
	let s = "hello world".to_string();
	print_i32(&i);
	print_string(&s);
     println!("{}",i);//编译通过,栈上的42的所有权借给print_i32用,但自身还是拥有42的所有权
     println!("{}",s);//编译通过,堆上hello world的所有权借给print_string用,但自身还是拥有hello world的所有权
}

小结

Rust所有权系统是Rust语言中的精华所在,也是Rust语法中最难学习的地方。上文只是一些粗浅的学习记录,很多地方不是很严谨,本人也将在后续的学习中继续补充相关内容。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!