Skip to content

Day 21: 智能指针:Box

在前面几天的学习中,我们已经掌握了Rust的所有权、借用和生命周期等核心概念。今天我们来学习Rust中另一种重要的类型——智能指针。智能指针是一类数据结构,它们不仅拥有数据的所有权,还提供了额外的元数据和功能。Box是最基础也是最常用的智能指针。

Rust的智能指针与C++的智能指针类似,但Rust的所有权模型使得智能指针的设计更加安全、清晰。Box是”堆上分配”(heap allocation)的桥梁,让我们能够在堆上存储数据,而不是栈上。

Box是最简单的智能指针,它将数据存储在堆上,而栈上只保留一个指向堆内存的指针。

fn main() {
let b = Box::new(5);
println!("Box中的值: {}", b);
// 解引用访问
let b2 = Box::new(String::from("Hello, Box!"));
println!("字符串Box: {}", b2);
// Box<T>实现了Deref trait,所以可以使用*解引用
let x = 5;
let y = Box::new(5);
println!("x == *y: {}", x == *y);
}

Box::new()函数会分配堆内存并存储值。当Box被drop时(离开作用域),堆上的数据会自动释放,这就是”智能”之处——我们不需要手动调用free或delete。

Rust编译器需要在编译时确定每个类型的大小。对于递归类型(自身包含自身引用的类型),直接定义可能会导致编译器无法确定大小。

考虑一个链表节点的定义:

// 错误:Enum无法确定大小
enum List {
Cons(i32, List), // 这会导致编译错误
Nil,
}

使用Box可以解决这个问题,因为Box的大小是固定的(就是一个指针的大小):

enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("链表长度计算完成");
}

Box将递归类型的大小从”无限”变为”固定”,因为Box在任何情况下都只占用一个指针的空间。

在Rust中,trait定义了行为接口。有时候我们需要在一系列不同类型上使用同一trait,这时就需要trait对象(trait object)。

trait对象是通过dyn Trait语法创建的,它代表任何实现了指定trait的类型。trait对象是运行时多态的,而泛型和monomorphization是编译时多态。

trait Draw {
fn draw(&self);
}
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
impl Draw for Circle {
fn draw(&self) {
println!("绘制圆形,半径: {}", self.radius);
}
}
impl Draw for Square {
fn draw(&self) {
println!("绘制正方形,边长: {}", self.side);
}
}
fn main() {
// 静态分发:泛型方式
fn draw_static<T: Draw>(shape: &T) {
shape.draw();
}
// 动态分发:trait对象方式
fn draw_dynamic(shape: &dyn Draw) {
shape.draw();
}
let circle = Circle { radius: 5.0 };
let square = Square { side: 4.0 };
draw_static(&circle);
draw_static(&square);
draw_dynamic(&circle);
draw_dynamic(&square);
}

dyn Trait返回的是胖指针(fat pointer),包含两个指针:指向数据的指针和指向vtable(虚函数表)的指针。vtable存储了trait方法的实现地址。

最常见的模式是将Box和dyn Trait组合使用,这样就能创建异构集合——同一个集合中可以存放不同类型的对象,只要它们实现了相同的trait。

trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
struct Duck;
impl Animal for Dog {
fn speak(&self) { println!("汪!"); }
}
impl Animal for Cat {
fn speak(&self) { println!("喵~"); }
}
impl Animal for Duck {
fn speak(&self) { println!("嘎嘎!"); }
}
fn main() {
// 创建动物集合
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Duck),
];
for animal in &animals {
animal.speak();
}
}

这段代码展示了Rust中运行时多态的强大能力。我们可以在一个Vec中存储不同类型的动物,只要它们都实现了Animal trait。

除了上述场景,Box还有以下重要用途:

fn main() {
// 1. 函数返回 trait 对象
fn create_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x * 2)
}
let closure = create_closure();
println!("闭包结果: {}", closure(5));
// 2. 避免栈溢出:对于大型数据结构,可以放在堆上
let large_array = Box::new([0u8; 10000]);
// 3. Box实现了Sized,所以可以作为函数参数
fn process_box(b: Box<[u8]>) {
println!("处理了 {} 字节", b.len());
}
process_box(large_array);
}

注意Box和Box的区别:前者是切片trait对象,大小在运行时确定。

今天我们学习了Box智能指针及其应用场景。Box的核心价值在于:

  • 在堆上分配数据,适合大型数据结构
  • 解决递归类型的大小问题
  • 支持动态分发(dyn Trait)

Box是Rust智能指针家族中最基础的一员,后续我们还会学习Rc、RefCell、Arc等更复杂的智能指针,它们各自解决了不同场景下的所有权问题。