Android程序员初学Rust-智能指针

0 阅读4分钟

1.png

Rust 中,智能指针是管理内存所有权和生命周期的核心工具。Rust 中提供了多种智能指针,包含:BoxRc(Reference Counting)、Arc(Atomic Reference Counting),Cell 等。

今天,我们先讲两个——BoxRc

Box

2.jpg

Box 是指向堆上数据的拥有所有权的指针:

fn main() {
    let five = Box::new(5);
    println!("five: {}", *five);
}

// Output
// five: 5

如图,它在内存中大概是这样:

3.png

Box<T> 实现了 Deref<Target = T>,这意味着你可以直接在 Box<T> 上调用 T 的方法。

递归数据类型或动态大小的数据类型,如果不使用指针间接引用,就无法内联存储。

Box 实现了这种间接引用:

#[derive(Debug)]
enum List<T> {
    /// 一个非空列表:第一个元素和列表的其余部分。
    Element(T, Box<List<T>>),
    /// 一个空列表。
    Nil,
}

fn main() {
    let list: List<i32> =
        List::Element(1, Box::new(List::Element(2, Box::new(List::Nil))));
    println!("{list:?}");
}

// Output
// Element(1, Element(2, Nil))

它的内存格式是这样:

4.png

Box 类似于 C++ 中的 std::unique_ptr,只不过它保证不为空。

当你遇到以下情况时,Box 会很有用:

  • 当你有一个在编译时无法确定大小,但 Rust 编译器又需要确切大小的类型时。
  • 当你想要转移大量数据的所有权时。为了避免在栈上复制大量数据,可以将数据存储在堆上的 Box 中,这样就只需移动指针。

如果不使用 Box,而是尝试将 List 直接嵌入到 List 中,编译器将无法计算出该结构体在内存中的固定大小(嵌套会导致 List 的大小将是无限的)。

Box 解决了这个问题,因为它的大小与普通指针相同,并且仅指向堆中 List 的下一个元素。

虽然 Box 在形式上与 C++ 中的 std::unique_ptr 类似,但它不能为空。这使得 Box 成为允许编译器对某些枚举类型进行存储优化(即 “小众优化”)的类型之一。

Rc

5.jpg

Rc 是一个引用计数的共享指针。当你需要在多个地方引用相同的数据时,可以使用它:

use std::rc::Rc;

fn main() {
    let a = Rc::new(10);
    let b = Rc::clone(&a);

    dbg!(a);
    dbg!(b);
}

// Output
// [src/main.rs:7:5] a = 10
// [src/main.rs:8:5] b = 10

每个 Rc 都指向同一个共享数据结构,该数据结构包含强指针、弱指针以及值:

5.png

Rc 的引用计数确保只要存在引用,其包含的值就一直有效。

Rust 中的 Rc 类似于 C++ 中的 std::shared_ptr

Rc::clone 操作开销较低:它创建一个指向相同内存分配的指针,并增加引用计数。它不会进行深度克隆,在排查代码性能问题时通常可以忽略不计。make_mut 实际上会在必要时克隆内部值(“写入时克隆”)并返回一个可变引用。

使用 Rc::strong_count 检查引用计数。Rc::downgrade 会返回一个弱引用计数对象,用于创建能被正确释放的循环结构。

特征对象的所有权

6.jpg

我们之前了解了如何将特征对象与引用一起使用,例如 &dyn Pet。不过,我们也可以将特征对象与像 Box 这样的智能指针一起使用,来创建一个拥有所有权的特征对象:Box<dyn Pet>

struct Dog {
    name: String,
    age: i8,
}
struct Cat {
    lives: i8,
}

trait Pet {
    fn talk(&self) -> String;
}

impl Pet for Dog {
    fn talk(&self) -> String {
        format!("Woof, my name is {}!", self.name)
    }
}

impl Pet for Cat {
    fn talk(&self) -> String {
        String::from("Miau!")
    }
}

fn main() {
    let pets: Vec<Box<dyn Pet>> = vec![
        Box::new(Cat { lives: 9 }),
        Box::new(Dog { name: String::from("Fido"), age: 5 }),
    ];
    for pet in pets {
        println!("Hello, who are you? {}", pet.talk());
    }
}

pets 的内存布局如下:

图片.png

实现给定特征的类型可能具有不同的大小。这使得在上例中无法使用 Vec<dyn Pet> 之类的东西。

dyn Pet 是一种向编译器说明实现 Pet 特征的动态大小类型的方式。

在示例中,pets 在栈上分配,而向量数据在堆上。向量的两个元素是胖指针。

胖指针是一种双宽度指针。它有两个组件:一个指向实际对象的指针,以及一个指向该特定对象的 Pet 实现的虚方法表(vtable)的指针。

我们可以通过如下代码,查看相关对象的大小:

fn main() {
    println!("{} {}", std::mem::size_of::<Dog>(), std::mem::size_of::<Cat>());
    println!("{} {}", std::mem::size_of::<&Dog>(), std::mem::size_of::<&Cat>());
    println!("{}", std::mem::size_of::<&dyn Pet>());
    println!("{}", std::mem::size_of::<Box<dyn Pet>>());
}

输出如下:

// Output
32 1
8 8
16
16