Rust:通过几个例子学习 Box

简介

《Rust:认识各种盒子吧!(Box, Rc, Arc, Cell, RefCell)》一文中,我们初步了解了各种容器。这篇文章希望通过丰富的例子加深理解。

场景

场景 1:动态大小类型(DST)

 1trait Animal {
 2    fn make_sound(&self);
 3}
 4
 5struct Dog;
 6
 7impl Animal for Dog {
 8    fn make_sound(&self) {
 9        println!("Woof!");
10    }
11}
12
13fn main() {
14    let dog: Box<dyn Animal> = Box::new(Dog);
15    dog.make_sound();
16}

这里的dyn Animal是一种动态调度的特征对象,表示一个实现了Animal特征的类型,但在编译时并不确定具体是哪种类型。相反,类型信息会在运行时通过动态分配来确定。动态特征对象的实现原理是虚函数表。因为特征对象的大小在编译时是不确定的,所以需要用Box、Rc之类的方式来封装。

使用枚举也可以实现类似的功能,某些情况下枚举可能是更好的选择。枚举在编译时确定了所有可能的类型,因此可以避免动态调度带来的性能开销。

但是,不是所有情况都能用枚举,比如你希望调用实现了trait的对象,但具体实现在其它库中。这种情况下还是只能用dyn

场景 2:递归数据结构

 1enum List {
 2    Cons(i32, Box<List>),
 3    Nil,
 4}
 5
 6use List::{Cons, Nil};
 7
 8fn main() {
 9    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
10}

为什么必须使用Box?考虑以下定义:

1enum List {
2    Cons(i32, List),
3    Nil,
4}

这个定义是无效的,因为它会导致无限递归。具体来说,Cons变体包含一个List,而List又包含一个Cons,以此类推。Rust编译器无法确定List类型的大小,因为它会无限递归下去。但是用Box的话,由于Box有固定的大小(在x64平台上是8字节指针),所以可以定义。

场景 3:转移所有权

1fn main() {
2    let x = Box::new(5);
3    let y = x; // 现在 y 拥有 x 的所有权
4    // println!("{}", x); // 这行代码会报错,因为 x 的所有权已经被转移
5    println!("{}", y);
6}

场景 4:减少栈内存使用

1struct LargeStruct {
2    data: [u8; 1024],
3}
4
5fn main() {
6    let large = Box::new(LargeStruct { data: [0; 1024] });
7    // large 现在存储在堆上,而不是栈上
8}

这样做的好处:

  1. 减少栈上的内存占用:使用Box可以将大数据结构的内存从栈转移到堆,从而减少栈上的内存占用。一般栈的大小有限,这样可以节省很多栈内存。

  2. 数据传递的效率:将这个大结构体传递给函数或在多个地方使用时,传递Box<LargeStruct>只涉及传递一个指针,效率更高。

场景 5:实现单例模式

 1struct Singleton {
 2    value: i32,
 3}
 4
 5impl Singleton {
 6    fn instance() -> &'static mut Box<Singleton> {
 7        static mut INSTANCE: Option<Box<Singleton>> = None;
 8        unsafe {
 9            INSTANCE.get_or_insert_with(|| Box::new(Singleton { value: 0 }))
10        }
11    }
12}
13
14fn main() {
15    let singleton = Singleton::instance();
16    singleton.value = 42;
17    println!("Singleton value: {}", singleton.value);
18}

这里用Box是合理的,因为单例要求生命周期与程序相同,而堆上的数据可以在整个程序运行期间保持有效。不过这个版本还不线程安全,下面是线程安全的版本:

 1use std::sync::{Mutex, Once};
 2
 3struct Singleton {
 4    value: i32,
 5}
 6
 7impl Singleton {
 8    fn instance() -> &'static Mutex<Singleton> {
 9        static mut INSTANCE: Option<Mutex<Singleton>> = None;
10        static INIT: Once = Once::new();
11        
12        unsafe {
13            INIT.call_once(|| {
14                let singleton = Singleton { value: 0 };
15                INSTANCE = Some(Mutex::new(singleton));
16            });
17            INSTANCE.as_ref().unwrap()
18        }
19    }
20}
21
22fn main() {
23    let singleton = Singleton::instance();
24    {
25        let mut singleton = singleton.lock().unwrap();
26        singleton.value = 42;
27    }
28    {
29        let singleton = singleton.lock().unwrap();
30        println!("Singleton value: {}", singleton.value);
31    }
32}

几个比较重要的方法

下面来了解 Box::from_rawBox::into_rawBox::pin

Box::from_raw

Box::from_raw 用于将一个裸指针(raw pointer)转换回 Box<T>。这个方法常用于将先前通过 Box::into_raw 变成裸指针的内存重新转换成 Box<T>,以便 Rust 可以再次管理和释放它。

示例

1fn main() {
2    let boxed = Box::new(5);
3    let raw = Box::into_raw(boxed); // 转换成裸指针
4    let boxed_again = unsafe { Box::from_raw(raw) }; // 重新转换成 Box
5
6    println!("Boxed value: {}", boxed_again);
7    // 当 boxed_again 超出作用域时,内存将被自动释放
8}

Box::into_raw

Box::into_raw 将一个 Box<T> 转换成一个裸指针。这在需要与非安全(unsafe)代码交互或手动管理内存时非常有用。使用此方法后,原来的 Box<T> 实例不再负责管理那块内存,必须确保稍后使用 Box::from_raw 重新获取所有权。

示例

 1fn main() {
 2    let boxed = Box::new(5);
 3    let raw = Box::into_raw(boxed); // 转换成裸指针
 4
 5    // 此时 boxed 已不再管理那块内存
 6    // 必须使用 Box::from_raw 重新获取所有权,否则会导致内存泄漏
 7    let boxed_again = unsafe { Box::from_raw(raw) };
 8
 9    println!("Boxed value: {}", boxed_again);
10    // 当 boxed_again 超出作用域时,内存将被自动释放
11}

Box::pin

Box::pin 用于将一个值固定在内存中的特定位置。这对于需要确保对象在内存中的位置不变的场景非常有用,尤其是在处理自引用(self-referential)类型时。

Pin<Box<T>> 可以确保 T 的内存地址在其生命周期内不会改变,这对于某些异步编程和生成器(generator)场景非常重要。

示例

 1use std::pin::Pin;
 2
 3struct SelfReferential {
 4    value: String,
 5    ptr: *const String,
 6}
 7
 8impl SelfReferential {
 9    fn new(value: String) -> Self {
10        Self {
11            ptr: &value,
12            value,
13        }
14    }
15
16    fn new_pinned(value: String) -> Pin<Box<SelfReferential>> {
17        let mut boxed = Box::pin(SelfReferential {
18            ptr: std::ptr::null(),
19            value,
20        });
21
22        // 安全地获取指向 value 的指针
23        let ptr = &boxed.value as *const String;
24        unsafe {
25            let mut_ref = Pin::as_mut(&mut boxed);
26            Pin::get_unchecked_mut(mut_ref).ptr = ptr;
27        }
28        
29        boxed
30    }
31}
32
33fn main() {
34    // 使用 new, ptr 可能会在移动时失效
35    let obj = SelfReferential::new(String::from("Hello"));
36    assert_eq!(obj.ptr, &obj.value as *const String); // 不安全
37
38    // 使用 new_pinned, ptr 保持有效
39    let pinned_obj = SelfReferential::new_pinned(String::from("World"));
40    assert_eq!(unsafe { &*pinned_obj.ptr }, &pinned_obj.value); // 安全
41}

作用总结

  • Box::from_raw: 将裸指针转换回 Box<T>,使 Rust 再次管理内存。

  • Box::into_raw: 将 Box<T> 转换成裸指针,以便与非安全代码交互或手动管理内存。

  • Box::pin: 创建一个 Pin<Box<T>>,确保 T 在内存中的地址在其生命周期内不变,适用于需要固定内存位置的场景。

再另一篇介绍 Rc、Arc 和 Weak 的文章中,我们会用到前两个方法实现 Rc.