不管是计算机的哪种语言,都有内存的管理方式。主流有两种,一是以C为代表的由开发者来决定申请和释放内存,二是以Python为代表的通过语言本身的垃圾回收机制来自动管理内存。Rust开辟了第三种方式,通过所有权系统管理内存。
Rust所有权规则,下面的例子都将围绕下面的3条所有权规则来展开讲解
1 | Rust 中的每一个值都有一个被称为其 所有者(owner)的变量 |
2 | 值在任一时刻有且只有一个所有者 |
3 | 当所有者(变量)离开作用域,这个值将被丢弃 |
一:变量作用域
rust">fn main() {let s = String::from("hello"); // 从此处起,s 开始有效println!("{}", s); // 使用 s
} // 此作用域已结束,// s 不再有效
第一部分由我们完成:当调用 String::from
时,它的实现(implementation)请求其所需的内存。这在编程语言中是非常通用的
第二部分实现起来就各有区别了。在有 垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确地为一个 allocate
配对一个 free
。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。当 s
离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String
的作者可以放置释放内存的代码。Rust 在结尾的 }
处自动调用 drop
。
二:移动
通过两个例子来对比,
rust">let x = 5;
let y = x;
println!("x={},y={}", x,y);
运行结果:
rust">let s1 = String::from("hello");
let s2 = s1;
println!("s1={},s2={}", s1,s2);
运行结果:
同样的变量赋值,为什么会产生上面的差异
前者,将 5
绑定到 x
;接着生成一个值 x
的拷贝并绑定到 y
。现在有了两个变量,x
和 y
,都等于 5
。这也正是事实上发生了的,因为整数是有已知固定大小的简单值,所以这两个 5
被放入了栈中
后者的行为会不会和前面x,y的赋值逻辑是一致的呢?从结果上看明细是有差异的
String
由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
长度表示 String
的内容当前使用了多少字节的内存。容量是 String
从分配器总共获取了多少字节的内存。当我们将 s1
赋值给 s2
,String
的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。
之前我们提到过当变量离开作用域后,Rust 自动调用 drop
函数并清理变量的堆内存。不过图 4-2 展示了两个数据指针指向了同一位置。这就有了一个问题:当 s2
和 s1
离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。在 let s2 = s1
之后,Rust 认为 s1
不再有效,因此 Rust 不需要在 s1
离开作用域后清理任何东西,如果这时候再使用s1就会报错,所以上面的图要修改一下
三:克隆
rust">let s1 = String::from("hello");
let s2 = s1.clone();println!("s1 = {}, s2 = {}", s1, s2);
运行结果:
克隆就是复制一份数据出来,在堆上有两处hello的数据
在eg2中,x和y并没有调用clone,但是x也没移动到y,原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y
后使 x
无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone
并不会与通常的浅拷贝有什么不同,我们可以不用管它
Rust 有一个叫做 Copy
trait 的特殊标注,可以用在类似整型这样的存储在栈上的类型上(第 10 章详细讲解 trait)。如果一个类型实现了 Copy
trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy
标注,将会出现一个编译时错误。要学习如何为你的类型添加 Copy
标注以实现该 trait,请阅读附录 C 中的 “可派生的 trait”。
那么哪些类型实现了 Copy
trait 呢?你可以查看给定类型的文档来确认,不过作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy
,任何不需要分配内存或某种形式资源的类型都可以实现 Copy
。如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都实现
Copy
的时候。比如,(i32, i32)
实现了Copy
,但(i32, String)
就没有。
四:所有权与函数
rust">fn main() {let s = String::from("hello"); // s 进入作用域takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效let x = 5; // x 进入作用域makes_copy(x); // x 应该移动函数里,// 但 i32 是 Copy 的,所以在后面可继续使用 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 移出作用域。不会有特殊操作
经常写C语言的在这里可能会非常不适应,变量传给函数,在C语言中是值传递,变量还是可以继续使用的,但是在Rust中这里和C有很大的区别。s作为函数传参进takes_ownership后s就转移失效了,但是如果函数后我想继续使用s,怎么办?可以通过返回值将s转移出来
五:返回值和作用域
rust">fn main() {let s1 = gives_ownership(); // gives_ownership 将返回值// 移给 s1let s2 = String::from("hello"); // s2 进入作用域let s3 = takes_and_gives_back(s2); // s2 被移动到// takes_and_gives_back 中,// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,// 所以什么也不会发生。s1 移出作用域并被丢弃fn gives_ownership() -> String { // gives_ownership 将返回值移动给// 调用它的函数let some_string = String::from("yours"); // some_string 进入作用域some_string // 返回 some_string 并移出给调用的函数
}// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域a_string // 返回 a_string 并移出给调用的函数
}
但是如果我们每次都是通过返回值来获取入参的所有权,这样就太蠢了,在Rust中可以通过引用来避免该问题