这是【零基础 Rust 入门】系列的第 2 章。本系列由前端技术专家零弌分享。想要探索前端技术的无限可能,就请关注我们吧!🤗
- 🧑💻 我们是谁:支付宝体验技术部-基础服务团队
- 📖 我们的专栏:前沿视点
Rust 内存机制
Rust 以内存安全著称,有一套独特的声明式的内存管理方式,与传统的手动申请、释放内存、或者自动化的 gc 机制完全不同。
课前知识 - 栈与堆
栈是一个 LIFO 的队列,一般都是一个固定的大小,并且对象的内存分配是顺序的。而堆可以随着内存的使用逐渐增大,但是堆上空间的分配不是有序的而是随机的。
接来下我们将写几段代码来感受一下栈和堆的特性。
栈 - LIFO
首先来感受一下栈的顺序性。
enum AppleColor {
Red,
Green,
}
fn main() {
println!("Apple color size: {}", std::mem::size_of::<AppleColor>());
let green = AppleColor::Green;
let red = AppleColor::Red;
println!("green apple address: {:p}", &green);
println!("red apple address: {:p}", &red);
}
运行代码 cargo run
,代码的输出:
Apple color size: 1
green apple address: 0x7ff7b2628c2e
red apple address: 0x7ff7b2628c2f
有趣的是如果使用 cargo run --release
,代码的输出:
Apple color size: 1
green apple address: 0x7ff7ba191d97
red apple address: 0x7ff7ba191d96
谜底等待未来讲并发的时候再说。
栈 - 固定大小
让我们造一个递归来让 Rust 体验一把 stack overflow。
enum AppleColor {
Red,
Green,
}
fn create_apple_color(times: u64) {
let green = AppleColor::Green;
println!("green apple address: {:p}", &green);
if times > 1 {
create_apple_color(times - 1)
}
}
fn main() {
create_apple_color(74813);
}
在 intel macos(14.2.1)使用 cargo run --release
可以得到 stack overflow 的效果,改成 74812
则不会 stack overflow。每次方法调用都会消耗栈的空间,而栈的空间是有限的,所以最后会挂掉。
green apple address: 0x7ff7ba73a607
green apple address: 0x7ff7ba73a597
green apple address: 0x7ff7ba73a527
green apple address: 0x7ff7ba73a4b7
green apple address: 0x7ff7ba73a447
green apple address: 0x7ff7ba73a3d7
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
[1] 53935 abort cargo run --release
堆 - 随机
以下代码通过使用 Box
来将对象分配在了堆上,可能可以观察到对象地址随机分布的情况(取决于操作系统当前的心情)。
use std::boxed::Box;
#[derive(Copy, Clone)]
enum AppleColor {
Red,
Green,
}
fn main() {
let mut colors: Vec<Box<AppleColor>> = vec![];
for i in 0..10 {
// 通过使用 Box 来将对象分配在堆上
let green = Box::new(AppleColor::Green);
println!("box address: {:p}", &green);
println!("green apple address: {:p}", &*green);
colors.push(green);
}
}
以下分别是两种情况的打印。
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ffc000
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff8000
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff8010
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4000
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4010
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4020
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4030
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4040
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4050
box address: 0x7ff7b6f6ed80
green apple address: 0x600003ff4060
box address: 0x7ff7b54c6d80
green apple address: 0x600000240000
box address: 0x7ff7b54c6d80
green apple address: 0x600000240010
box address: 0x7ff7b54c6d80
green apple address: 0x600000240020
box address: 0x7ff7b54c6d80
green apple address: 0x600000240030
box address: 0x7ff7b54c6d80
green apple address: 0x600000240040
box address: 0x7ff7b54c6d80
green apple address: 0x600000240050
box address: 0x7ff7b54c6d80
green apple address: 0x600000240060
box address: 0x7ff7b54c6d80
green apple address: 0x600000240070
box address: 0x7ff7b54c6d80
green apple address: 0x600000240080
box address: 0x7ff7b54c6d80
green apple address: 0x600000240090
栈和堆的使用方式
使用栈的方式返回对象。Apple 经历了多次拷贝:
- Apple::new
- get_apple_from_stack
最后生命周期终止在了 println! 宏这里。
use std::boxed::Box;
#[derive(Copy, Clone, Debug)]
enum AppleColor {
Red,
Green,
}
#[derive(Debug)]
struct Apple {
color: AppleColor,
}
impl Apple {
fn new(color: AppleColor) -> Self {
Apple {
color,
}
}
}
fn get_apple_from_stack() -> Apple {
Apple::new(AppleColor::Red)
}
fn main() {
let apple = get_apple_from_stack();
println!("get apple from stack: {:?}", apple);
}
使用堆的方式来获取对象。对象的生命周期:
- 使用 Box 创建了一块内存,可以放得下 Apple
- 在栈上创建了 Apple 对象
- 将 Apple 对象拷贝到了 Box 的内存空间中
- 最后生命周期终止在了 println! 宏这里
use std::boxed::Box;
#[derive(Copy, Clone, Debug)]
enum AppleColor {
Red,
Green,
}
#[derive(Debug)]
struct Apple {
color: AppleColor,
}
impl Apple {
fn new(color: AppleColor) -> Self {
Apple {
color,
}
}
}
fn get_apple_from_heap(a_box: &mut Box<Option<Apple>>) {
let apple = Apple::new(AppleColor::Red);
**a_box = Some(apple);
}
fn main() {
let mut apple_box = Box::new(None);
get_apple_from_heap(&mut apple_box);
println!("get apple from heap: {:?}", apple_box);
}
栈和堆的性能
栈不需要分配内存,堆需要分配内存。
栈适合拷贝小对象,堆适合被引用。
例如经典的 String 数据结构,String 在拷贝时仅会拷贝 ptr/len/capacity,并不会拷贝堆上的值,因此性能很快。
Rust 保护
rust 会对对象进行生命周期的保护,如果在生命周期之外引用一个对象,会导致编译失败。
比如在函数返回时,引用了栈上的一个值。
Copy/Clone/Move
当一个类型实现了 Copy
这个 trait 之后,赋值过后原来的值还是可用的。
如果类型中的有部分实现了 Drop
或者类型本身实现了 Drop
则不能实现 Copy
。
默认 Copy 的类型
- 所有整形
- 布尔
- 所有浮点型
- 字符类型
- Tuple (需要所有的元素实现 Copy)
- 数组(元素是 Copy 的话)
问:String 的赋值操作是 Copy 还是 Move?为什么?
回答:Move,如果是 Copy 则堆上的内存变成了有两个 owner。
所有权
Rust 中的对象只能有一个 owner,所有权可以转交但是不能同时有两个 owner。但是对象可以 borrow,可以有多个 immutable ref,但是只能有一个 mutable ref,并且 mutable ref 是排他的,正在 mutable borrow 时,不能有 immutable borrow,同里一旦有 immutable borrow 时不能有 mutable borrow。
用一个苹果举例,队长阿威有一个苹果,他有两个同事 小e 和 小k,可以有以下几种情况:
- 阿威在啃苹果,小e 和 小k 不可以看不可以啃。
-
- 内存损坏:如果这时候 小e 看了一眼,发现阿威还没啃,小e 上嘴啃就可能啃到阿威刚啃过的地方。
- 数据错乱:如果这时候 小e 看了一眼,发现阿威还没啃,心里计算我还有一个苹果可以吃,数据错乱。
- 阿威没有在啃苹果,小e 和 小k 可以一起看。
- 阿威没有在啃苹果,小e 和 小k 只有一个人可以借过去啃。
#[derive(Copy, Clone, Debug)]
enum AppleColor {
Red,
Green,
}
#[derive(Debug)]
struct Apple {
color: AppleColor,
kou: u8,
}
impl Apple {
fn new(color: AppleColor) -> Self {
Apple {
color,
kou: 10,
}
}
fn cost(&mut self) {
self.kou -= 1;
}
}
struct Person<'a, 'b> {
pub name: String,
pub apple: Option<Apple>,
pub borrowed_look_apple: Option<&'a Apple>,
pub borrowed_eat_apple: Option<&'b mut Apple>,
}
impl<'a, 'b> Person<'a, 'b> {
fn new_with_apple(name: String, apple: Apple) -> Self {
Person {
name,
apple: Some(apple),
borrowed_eat_apple: None,
borrowed_look_apple: None,
}
}
fn new(name: String) -> Self {
Person {
name,
apple: None,
borrowed_eat_apple: None,
borrowed_look_apple: None,
}
}
fn borrow_eat<'c, 'd>(&'d mut self, apple: &'c mut Apple) where 'c: 'b {
self.borrowed_eat_apple = Some(apple);
}
fn borrow_look<'c, 'd>(&'d mut self, apple:&'c Apple) where 'c: 'a {
self.borrowed_look_apple = Some(apple);
}
fn return_look(&mut self) {
self.borrowed_look_apple = None;
}
fn eat_borrowed(&mut self) {
self.borrowed_eat_apple
.as_mut()
.expect("must borrow apple")
.cost();
println!("eat borrowed apple succeed");
}
fn look(&self) -> u8 {
self.borrowed_look_apple
.as_ref()
.expect("must borrow apple")
.kou
}
fn eat(&mut self) {
self.apple
.as_mut()
.expect("must have apple")
.cost();
println!("eat owned apple succeed");
}
}
fn main() {
let mut w = Person::new_with_apple(String::from("阿威"), Apple::new(AppleColor::Red));
let mut e = Person::new(String::from("Elrrrrrrr"));
let mut k = Person::new(String::from("Kuitos"));
{
// 自己吃
w.eat();
}
// {
// // 同时借给两个人看
// let borrow_apple = w.apple.as_ref().expect("must have");
// e.borrow_look(borrow_apple);
// k.borrow_look(borrow_apple);
//
// e.return_look();
// k.return_look();
// }
// {
// // 借给别人吃
// let borrow_apple = w.apple.as_mut().expect("must have");
// e.borrow_eat(borrow_apple);
// e.eat_borrowed();
// }
// {
// // 别人在看的时候不能吃
// let borrow_apple = w.apple.as_ref().expect("must have");
// e.borrow_look(borrow_apple);
// w.eat();
// e.look();
// }
// {
// // 借给别人吃的时候自己不能吃
// let borrow_apple = w.apple.as_mut().expect("must have");
// e.borrow_eat(borrow_apple);
// w.eat();
// e.eat_borrowed();
// }
// {
// // 借给别人吃的时候不能借给别人看
// let borrow_eat_apple = w.apple.as_mut().expect("must have");
// let borrow_look_apple = w.apple.as_ref().expect("must have");
// e.borrow_eat(borrow_eat_apple);
// e.eat_borrowed();
// w.borrow_look(borrow_look_apple);
// w.look();
// }
}
课后习题:为什么这两个代码块不能同时解除注释?
// {
// // 同时借给两个人看
// let borrow_apple = w.apple.as_ref().expect("must have");
// e.borrow_look(borrow_apple);
// k.borrow_look(borrow_apple);
//
// e.return_look();
// k.return_look();
// }
// {
// let borrow_apple = w.apple.as_mut().expect("must have");
// e.borrow_eat(borrow_apple);
// e.eat_borrowed();
// }
答案:w.apple.as_ref() 取引用后,其生命周期和 e 一样长,导致 immutable ref 和 e 一起存活下去。
进阶问题:为什么 e borrow 之后还能 return?
答案:fn borrow_eat<'c, 'd>(&'d mut self, apple: &'c mut Apple) where 'c: 'b,这里额外标记了生命周期 d,因此引用的生命周期短于 e,下面还能再 borrow 一次 mutable ref。
进进阶:实现一个新的数据结构,支持这些方法可以一起跑。
使用 rust 内存的一百种姿势
- RC: 引用计数器一个对象能同时被多个对象持有,所有的 RC 释放后,内存释放
- Weak:和 RC 配合使用,避免循环引用出现
- RefCell:RC 不能被 mutable borrow,需要和 RefCell 配合使用
- Arc: 线程安全的 RC
- Box:将对象分配在堆上,Vec、String、HashMap 默认在堆上
进阶
PhantomData
doc.rust-lang.org/std/marker/…
use std::marker::PhantomData;
struct Slice<'a, T> {
start: *const T,
end: *const T,
phantom: PhantomData<&'a T>,
}
'a 的生命周期需要比 Slice 的生命周期的长,而如果没有 phantom 则无法表示,常用于 unsafe 代码如这里的指针。
PhantomPinned
use std::pin::Pin;
use std::marker::PhantomPinned;
use std::ptr::NonNull;
// This is a self-referential struct because the slice field points to the data field.
// We cannot inform the compiler about that with a normal reference,
// as this pattern cannot be described with the usual borrowing rules.
// Instead we use a raw pointer, though one which is known not to be null,
// as we know it's pointing at the string.
struct Unmovable {
data: String,
slice: NonNull<String>,
_pin: PhantomPinned,
}
Unmovable 中的 slice 指向了自己的 data 字段(这是一个指针),一旦 Unmovable 在内存中发生了移动,如 swap,就会导致 slice 的指向失效。而 PhantomPinned 就告诉了 rust 这块内存是不能动的。