Rust 所有权和使用
所有程序都需要和内存打交道,有三种流派
- 垃圾回收机制(gc),程序运行时不断寻找不在使用的内存,如 Java,go
- 手动管理内存的分配释放,如 C++
- 通过所有权来关系内存,编译器在编译的时候进行检查
所有权
不同于 go 语言的 gc,Rust 使用第三种 所有权系统,只在编译时检查,不在运行期检查,不会有性能损失。
栈和堆
栈和堆都是为了给程序提供可以使用的内存空间。
栈
栈中所有的数据都是固定大小的内存空间。
堆
与栈相对,当存储大小未知或可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
基于栈只是放新数据到栈顶,而堆需要找足够的内存空间;而且栈数据通常可以直接存储到 cpu 缓存,而堆只能在内存中;处理器处理和分配在栈上数据会比在堆上的数据更加高效。
所有权和堆栈
当调用函数时,传给函数的参数依次被压入栈中,当函数调用结束时,这些值将被从栈中按照相反的顺序依次移除。
因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏。这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。
所有权原则
- Rust 中每一个值都被一个变量所拥有,该变量被称之为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃 (drop)
变量绑定背后的数据交互
let x = 5;
let y = x;
将 5 绑定到变量 x;接着拷贝 x 的值赋给 y,最终 x 和 y 都等于 5,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存
let s1 = String::from("hello");
let s2 = s1;
String 不是基本类型,存储在堆上,不能自动拷贝
String 是复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成。
对于 let s2 =s1
有两种情况
- 对数据进行深拷贝,无论是 String 本身还是底层的堆上数据,都会被全部拷贝,性能问题大
- 只拷贝 String 本身,这样很快,但是两个 String 都指向堆上该数据,数据由两个所有者,当离开作用域时,会出现重复二次问题。
因此rust 中,当 s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了;在转移所有权后,s1 也就不能再被使用了。
即如果操作或访问 s1 则会 value used here after move
克隆
首先,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
如果我们确实需要深度复制 String 中堆上的数据,可以使用clone
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
拷贝
浅拷贝只发生在栈上,因此性能很高
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
函数传值与返回
将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样
引用和借用
仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂,Rust 也可以像其它编程语言一样,使用某个变量的指针或者引用。
Rust 通过 借用(Borrowing) 这个概念来达成上述的目的,获取变量的引用,称之为借用。
引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址。
和其他语言一样,引用 &
,解引用 *
不可变引用
引用允许你使用值,但是不获取所有权。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传引用
println!("The length of '{}' is {}.", s1, len); // s1 还可以使用
}
fn calculate_length(s: &String) -> usize { // 类型为 &String
s.len()
}
通过 &s1 语法,我们创建了一个指向 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。
就如变量不可变,引用指向的值默认也是不可变的。
fn main() {
let s = String::from("hello"); // String 不可变
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
可变引用
首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
可变引用同时只能存在一个,同一作用域,特定数据只能有一个可变引用;为了使 Rust 在编译期就避免数据竞争,再检查时,检查到最后一次使用
可变引用与不可变引用不能同时存在,为了防止不可变的引用被需要了内容
悬垂引用
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器可以确保数据不会在其引用之前被释放,要想释放数据,必须先停止其引用的使用。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 此处 s 离开作用域,内存被释放。
}
解决方法是直接返回 String:
fn no_dangle() -> String {
let s = String::from("hello");
s //String 的 所有权被转移给外面的调用者
}
借用规则如下:
- 同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
- 引用必须总是有效的