【零基础 Rust 入门 02】所有权 - 我有一个苹果

549 阅读10分钟

这是【零基础 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);
}

使用堆的方式来获取对象。对象的生命周期:

  1. 使用 Box 创建了一块内存,可以放得下 Apple
  2. 在栈上创建了 Apple 对象
  3. 将 Apple 对象拷贝到了 Box 的内存空间中
  4. 最后生命周期终止在了 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 这块内存是不能动的。