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}
这样做的好处:
-
减少栈上的内存占用:使用Box可以将大数据结构的内存从栈转移到堆,从而减少栈上的内存占用。一般栈的大小有限,这样可以节省很多栈内存。
-
数据传递的效率:将这个大结构体传递给函数或在多个地方使用时,传递
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_raw
、Box::into_raw
和 Box::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.