简单理解Rust的所有权
简单理解Rust的所有权
Rust是无GC的语言
Rust 不需要一个在程序运行时定期扫描内存并自动回收垃圾的机制(即垃圾回收器,Garbage Collector)。它通过一套独特的所有权(Ownership)系统,在编译期就严格规定了谁拥有内存、谁有权限修改、以及内存何时应该被释放。这套规则由编译器检查,确保了内存安全,而无需运行时 GC 的介入。
系统资源都被唯一绑定在某一个变量身上,出了作用域直接销毁,开发者无需手动调用free()
(像在 C/C++ 中那样),也无需依赖 GC 来寻找并清理垃圾,这样一来它的运行速度就可以做到巨快无比(因为抛掉了GC),作为系统语言它比C/C++都要快得多。
有GC的例子
在Java中:
public class GCDemo {
public static String getMessage() {
String message = "Hello, World!";
return message;
}
public static void main(String[] args) {
String myMessage = getMessage();
System.out.println(myMessage);
// 之后不再使用 myMessage
}
}
在getMessage
方法中,字符串"Hello, World!"
在堆上被创建,main
函数使用完myMessage
后,这个引用被置空或随着方法结束而失效,但此时那个字符串对象仍然占据着内存,它已经成了“垃圾”。在未来的某个不确定的时间点,Java 的垃圾回收器会启动。它会暂停程序的运行,扫描整个堆内存,发现垃圾后回收内存。
在有GC语言中,内存的回收是运行时的、自动但非即时的,由 GC 这个独立的组件负责。开发者不知道也不关心它具体何时发生。
无GC的例子
fn get_message() -> String {
let message = String::from("Hello, World!");
return message; // 所有权被移出函数
}
fn main() {
let my_message = get_message(); // my_message 现在拥有了字符串的所有权
println!("{}", my_message);
}
在main
函数中,my_message
是这块内存唯一的所有者。当my_message
在main
函数的末尾离开作用域}
时,Rust 编译器在编译时就已经确定这是它生命的终点,并自动在此处插入释放内存的代码。内存被立即且确定性地回收。
内存的回收是编译时决定好的,是即时且确定的。不需要一个运行时的垃圾回收器来周期性地查找和清理。
当然,你也可以试图强行在作用域外保持一块内存的引用,不过编译器不会让你这么做:
fn get_message_bad() -> &str { // 错误!缺少生命周期标识符
let message = String::from("Hello, World!");
return &message; // 返回一个局部变量的引用
}
这么写&message
成了一个指向已释放内存的悬垂指针,你又没告诉编译器啥时候可以释放掉,就会有编译错误。如果必须保留引用,也可以手动指定生命周期:
fn get_message_good<'a>(input_str: &'a str) -> &'a str {
input_str
}
函数 get_message_good
有一个生命周期参数 'a
。它接受一个引用 input_str
,这个引用至少要存活 'a
这么久。它返回一个引用,这个引用也至少能存活 'a
这么久。
简单理解就是把一个尚不确定生命周期的对象捆绑到另一个确定生命周期的对象身上,它不死你就不死,它死你也死。
用的时候:
fn main() {
let external_string = String::from("Hello from the outside!");
let my_message = get_message_good(&external_string);
// my_message 的生命周期 <= external_string 的生命周期
println!("{}", my_message);
} // external_string 在这里被 drop,my_message 也随之失效
external_string
活到哪,get_message_good
的返回值就活到哪,my_message
也活到哪。
也可以通过返回字符串字面量(&'static str
)让这个引用返回值永久有效:
fn get_message() -> &'static str {
"Hello, World!"
}
对所有权的大概解释
从上面的描述可以看出,rust对于内存回收既不像c++那样全靠自觉,也不像Java一样全程保姆,而是在代码层面规定了一套独特的语法,让你写的时候就写不出垃圾来,这就是所有权的意义。
所有权移交
既然变量离开作用域后堆内存就立刻被回收,那变量之间就无法再共享对象,这个问题无法解决,所以rust规定变量不能共享对象,赋值这种浅拷贝操作自然就不行。
如果你试图用=
来赋值,比如
let p: Point = Point { x: 1, y: 2 };
let p2: Point = p;
那实际上就是把p指向的这块栈内存剪切并粘贴到了p2指向的栈空间。这时候如果试图使用p的值会报错,不过你仍然可以给p赋别的值甚至把别的对象移交给它(前提是p是mut的),p相当于进入了未初始化的状态。
如果是动态内存的结构比如可变数组,在栈上只存储一个定长的list,包含堆起始位置、长度、cap信息,移交给别的变量时只需要移动这个信息即可。
引用类型
这种类型是为了解决对象复制问题的,相当于受限的指针——它必须有具体类型、不可修改(初始化之后不能再移位置)、初始化。这种类型存储时用u64
,只存堆起始地址和长度。有了这个就可以解决对象经常容易消失的问题:
fn inspect(list: &Vec<i32>) {
println!("The data is: {:?}", list);
}
fn main() {
let data = vec![1, 2, 3];
inspect(&data);
println!("The data is: {:?}", data);
}
如果没有引用,data当参数传递到函数里面,函数执行完就出了参数的作用域,data直接就消失了。
借用
分为可变和不可变,不可变的就是上边的例子,可变的就是你可以通过这个引用来修改对象的值:
let mut value: &i32 = 10;
let ref_value: &mut i32 = &mut value;
*ref_value += 5;
println!("The value is: {:?}", value); // 15
引用类型和原始类型是不同的类型,但是你仍然可以直接在引用类型上调用原始类型的方法,这其实是一个自动解引用的语法糖,后续再放。