所有权规则
RUST引入了一个新的概念——所有权来进行内存管理。通过所有权机制,RUST开发者就不用经常考虑数据是存储在堆上还是栈上以及在什么时候需要及时释放内存等操作。所有权规则如下:
- RUST中每个值都有一个所有者
- 一次只能有一位所有者
- 当所有者超出范围时,该值将被删除
变量范围、内存和分配
RUST中变量的有效范围其实和其他语言相差不大,都是从变量声明开始时生效,到离开变量作用域时失效。不过不像C/C++语言一样如果存在堆上的数据需要手动释放而且申请内存操作和释放内存操作要一一对应。RUST变量不论存储在堆上还是栈上,超出作用范围时都会被删除,原因是RUST在变量超出范围时会自动调用一种特殊函数 drop 来回收内存,有点像C++类中的析构函数。
变量与数据的交互方式
RUST中变量与数据的交互方式有两种:移动(MOVE)和克隆(CLONE)。首先介绍一下移动。观察下面的代码:
let x = 5;
let y = x;
现在我们将得到两个变量 x 和 y ,它们的值都是5,而且都在生效。这在RUST中是被允许的,因为它们都是基本数据类型,被存储在栈中。像这样的基本数据类型还有所有整数类型、所有浮点类型、布尔类型、字符类型和仅包含以上类型的元组。RUST给这些类型标注了一种称为copy的特征,这类型的数据移动方式就是直接复制。
但是如果数据是存储在堆上,那么数据的移动方式就会有所不同,比如下面这段代码中有两个string类型变量:
let s1 = String::from("hello");
let s2 = s1;
看上去与第一个例子没什么两异,但是实际上这里的 s1 已经无效了,它的值已经完全被移动到 s2 中去了。下图表示了上面这段代码执行后的正确结果:
我们可以看看如果不是这样会发生什么,比如 s1 和 s2 同时生效,像下面这张图一样:
两个指针指向同一块堆上的内存,在内存释放时会发生重复释放内存的错误。如果两个指针指向的是不同的两块内存呢,像深拷贝一样,比如下面这张图:
理论上是可行的,但是RUST认为这样会影响运行性能,因为如果是一大块数据,这样复制显然很耗费时间。不过RUST并没有完全禁止深拷贝,只不过使用了一种称为克隆的方法:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
上面这段代码就实现了深拷贝那张图的效果,两个变量都会生效,而且指向不同的两块内存。
涉及函数的所有权机制
下面这段代码展示函数的所有权机制是怎么运行的:
fn main() {
let s = String::from("hello"); // s 被声明开始生效
takes_ownership(s); // s 的值被移动到函数中,从这里
// 开始 s 已经失效
let x = 5; // x 被声明开始生效
makes_copy(x); // x 的值被移动到函数中,但是因
// 为基本类型的移动是直接复制,
// 因此 x 仍然生效
} // x 超出作用范围,然后是 s,因为 s 已经失效,所以无需重复释放
fn takes_ownership(some_string: String) { // some_string 通过传入的值开始生效
println!("{}", some_string);
} // some_string 超出作用范围,调用 drop 函数回收内存
fn makes_copy(some_integer: i32) { // some_integer 通过传入的值开始生效
println!("{}", some_integer);
} // some_integer 超出作用范围,但是因为是 i32 类型,所以无需被删除释放
函数返回值的所有权机制
函数返回值也存在所有权机制,下面是一个示例demo:
fn main() {
let s1 = gives_ownership(); // 把 gives_ownership 函数的返回值移动给 s1
let s2 = String::from("hello"); // s2 开始生效
let s3 = takes_and_gives_back(s2); // s2 被移动到 takes_and_gives_back 函数中,同时函数的返回值被移动给 s3
} // s3 超出作用范围被 drop掉, s2 已经失效,无需操作, s1 也超出作用范围被 drop掉
fn gives_ownership() -> String { // gives_ownership 将会移动它的返回值给调用它的表达式
let some_string = String::from("yours"); // some_string 开始生效
some_string // some_string 作为返回值被移动出函数
}
// 这个函数获取一个传入string,再把它返回
fn takes_and_gives_back(a_string: String) -> String { // a_string 开始生效
a_string // a_string 作为返回值被移动出函数
}
所以这又伴随着产生了一个问题,当我们函数使用任何一个变量时都会伴随着所有权的转移,而当我们再次使用它时,只能在函数中再次传回它,比如下面这个demo:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回 String 字符串的长度
(s, length)
}
s1 被移动到函数中后失效,为了打印时能打印这个字符串,不得已在函数中又将它作为返回值传回给 s2 。这虽然可行但显然麻烦了些,不过RUST也有引用这个概念,有效解决了上述情况。