zhengrenzhe's blog   About

Rust 所有权机制

Rust 有一种特殊的保证内存安全的机制,称为“所有权机制”,它与GC或手动管理内存的方式不同,可以做到在编译期对内存使用进行检查,以防止运行时的内存错误,并且所有权机制不会导致运行时开销。

Stack 与 Heap

在学习所有权机制之前,需要了解两个程序运行时的概念:堆(Heap)与栈(Stack)。

堆与栈都是程序在运行时可使用的内存,他们的区别可以简单理解为:堆用于存储非固定大小的对象,例如Rust中的String,它的长度是可变的;而栈用于存储固定大小的对象,例如i32,f64,bool,char的基础数据类型或是由基础数据类型组成的复合数据类型。同时,由于两种内存自身的差异以及所存储的对象的结构不同,堆的读写速度往往要低于栈的读写速度。

所有权机制的基本规则

所有权机制的规则有三:

第一次看到这些规则,可能会有一头雾水,每一个值指的是什么?所有者变量又是什么?我们来看下面这个例子。

fn main() {
    let str = String::from("hello, world");
    let str2 = str;
    println!("{}", str); // Error: value used here after move
    print(str2);
    println!("{}", str2); // Error: value used here after move
}

fn print(str: String) {
    println!("{}", str);
}

以上就是所有权机制的运行过程,接下来看一个在栈内存上的例子:

fn main() {
    let s1 = 12;
    let s2 = s1;
    println!("{}", s1);
    print(s2);
    println!("{}", s2);
}

fn print(s: i32) {
    println!("{}", s);
}

同样的模式,只是将String换成了i32,编译器就不会报错,看上去它并不遵守所有权的规则。在前面说过,i32等基础数据类型存放在栈上,而他们的读写速度是快速的,所以将s1赋值给s2时,实际上赋的值是s1的拷贝,s1与s2并不指向统一内存区域。所以所有权机制仍是工作的,只是在这里由于拷贝的原因并没有发生所有权转移。

引用

所有权机制是简单高效的,但也不是没有缺点。在多次函数调用中,所有权会发生多次转移,如果还想保持原有变量的所有权,就必须将函数内的变量返回出去,就像下面这样:

fn main() {
    let mut str = String::from("hello, world");
    str = print(str);
}

fn print(str: String) -> String {
    println!("{}", str);
    str
}

这显然是有些麻烦,有没有一种方法,只传递指针,但不传递所有权呢?Rust 的引用机制就是为此而生的。

fn main() {
    let str = String::from("hello, world");
    print(&str);
}

fn print(str: &String) {
    println!("{}", str);
}

使用&将形参标记为引用类型,再将实参标记为引用,这样就不会发生所有权转移了。但这只能对引用进行读取,如果需要对引用进行写操作,需要使用&mut来标记:

fn main() {
    let mut str = String::from("hello, world");
    print(&mut str);
}

fn print(str: &mut String) {
    str.push_str("!");
    println!("{}", str);
}

不过可变引用有一个限制:在同一作用域内,特定数据只能存在一种类型的引用,如果这种引用是可变引用,那只能存在一处可变引用。而不可变引用则可同时存在无数个

fn main() {
    let mut str = String::from("hello, world");

    let r1 = &mut str;
    let r2 = &mut str; // Error: second mutable borrow occurs here
}
fn main() {
    let mut str = String::from("hello, world");

    let r1 = &str;
    let r2 = &mut str; // Error: mutable borrow occurs here
}

这种限制是为了避免数据竞争,Rust会在编译期检测这些错误,以防止代码在运行时出现难以调试的bug。

还有一种引用问题是Rust可以在编译期发现的,就是悬垂引用。

fn print() -> &String {
    let s = String::from("hello");
    &s // Error: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
}

这里我们在函数内创建了一个字符串变量s,并返回它的引用,但当函数运行结束后,s指向的内存将被释放,但却返回了指向该内存区域的引用,这就是一个悬垂引用,Rust会在编译期检测这种问题。

不论是不可变的引用,还是可变的引用,它们都只指向创建引用的变量,而不拥有它,所以在引用离开作用域时,内存并不会被释放。

总结

到这里所有权机制的讲解就结束了,它是一个简单高效的保障运行时内存安全的系统,是学习Rust的基础。

← Rust 中的表达式与语句  Microsoft Chakra 嵌入使用指南 →