Rust:Box、match、ownership

Box 的核心概念

1. 堆分配

  • Box 将数据存储在上而不是栈上
  • 栈上只保留指向堆数据的指针
  • 解决栈空间有限的问题,特别是对于大对象

2. 所有权机制

  • Box 遵循 Rust 的所有权规则
  • Box 离开作用域时,会自动释放其内存
  • 实现 Drop trait,确保内存安全
1
2
3
4
{
let b = Box::new(5); // 在堆上分配
// 使用 b...
} // b 离开作用域,内存自动释放

为什么需要 Box

1. 处理递归类型

Rust 在编译时需要知道类型的大小,但递归类型(如链表)的大小无法在编译时确定:

1
2
3
4
5
6
7
8
enum List {
Cons(i32, Box<List>), // 使用 Box 固定大小
Nil,
}

use List::{Cons, Nil};

let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

2. 转移大数据所有权

当需要转移大数据的所有权时,使用 Box 只需复制指针而非整个数据:

1
2
3
4
5
6
fn take_ownership(data: Box<Vec<i32>>) {
// 处理数据...
}

let big_data = Box::new(vec![0; 1_000_000]);
take_ownership(big_data); // 高效转移所有权

3. 创建 trait 对象

Box 可以存储 trait 对象,实现动态分发:

1
2
3
4
5
6
7
8
9
10
11
12
trait Drawable {
fn draw(&self);
}

struct Circle;
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing a circle");
}
}

let shapes: Vec<Box<dyn Drawable>> = vec![Box::new(Circle)];

Box 的工作原理

内存布局

1
2
3
4
栈              堆
+------+ +-------+
| 指针 | ----> | 数据 |
+------+ +-------+

创建和使用

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建 Box
let boxed_i32 = Box::new(42);
let boxed_vec = Box::new(vec![1, 2, 3]);

// 访问数据
println!("{}", boxed_i32); // 自动解引用
println!("{:?}", boxed_vec[1]); // 自动解引用

// 模式匹配
match *boxed_i32 {
42 => println!("It's 42!"),
_ => println!("Something else"),
}

Box 的性能特点

  1. 零开销抽象

    • Box 本身没有运行时开销
    • 编译后就是普通的指针操作
  2. 内存成本

    • 每个 Box 占用一个指针的大小(通常 8 字节)
    • 加上堆分配的开销(分配器元数据)
  3. 使用场景

    • 适合中等大小的数据
    • 超大对象考虑其他方案(如内存映射文件)
    • 小对象可能更适合栈分配

Box 与其他智能指针对比

特性 Box<T> Rc<T> Arc<T>
所有权 单一所有权 共享所有权 线程安全共享
线程安全 是(Send) 是(Send+Sync)
开销 最低 引用计数 原子引用计数
主要用途 堆分配、递归类型 单线程共享数据 多线程共享数据

最佳实践

  1. 优先使用栈分配

    • 只在必要时使用 Box
    • 小对象和临时变量放栈上
  2. 避免过度使用

    • 不要用 Box 包装基本类型(除非必要)
    • 考虑替代方案如 Vec 或数组
  3. 内存泄漏防范

    • 避免循环引用(虽然 Box 本身不易泄漏)
    • 使用 Box::leak 时要特别小心
  4. 与模式匹配结合

    1
    2
    3
    4
    let boxed = Box::new(Some(42));
    if let Some(value) = *boxed {
    println!("Got {}", value);
    }

问题_1:

1
2
3
4
match *boxed_i32 {
42 => println!("It's 42!"),
_ => println!("Something else"),
}

这里的 boxed_i32 为什么编译器不能自动插入 *,需要手动写成 *boxed_i32,写成 boxed_i32 就报错?

这个问题涉及到 Rust 中模式匹配的核心机制,以及自动解引用在match表达式中的特殊行为。让我详细解释一下:

根本原因:模式匹配的特殊性

在 Rust 中,match表达式需要精确的类型匹配,不会自动解引用。这是因为:

  1. 模式匹配的本质

    • match 是解构操作,需要精确知道值的类型
    • 每个分支的模式必须与表达式的类型完全匹配
    • 自动解引用会破坏类型一致性
  2. 所有权语义

    • match 会消耗输入值的所有权
    • 自动解引用会改变所有权行为
    • 需要程序员显式决定如何处理所有权

示例分析

错误写法(不能自动解引用)

1
2
3
4
match boxed_i32 { // 类型是 Box<i32>
42 => ... // 类型是 i32
_ => ...
}

编译器会报错:

1
2
mismatched types
expected `Box<i32>`, found integer

正确写法(显式解引用)

1
2
3
4
match *boxed_i32 { // 显式解引用为 i32
42 => ... // 匹配 i32
_ => ...
}

为什么不能自动解引用

  1. 类型安全

    • Box<i32>i32 是不同类型
    • Rust 要求模式匹配必须类型精确匹配
    • 自动转换会破坏类型系统的一致性
  2. 所有权明确性

    • 解引用 Box 会移动内部值(如果未实现 Copy
    • 编译器需要你显式确认这个操作
    • 例如:
      1
      2
      3
      4
      5
      let s = Box::new(String::from("hello"));
      match *s { // 显式移动 String 出 Box
      _ => println!("String moved")
      }
      // 不能再使用 s
  3. 模式匹配的灵活性

    • 你可能需要匹配 Box 本身的结构
    • 例如:
      1
      2
      3
      4
      match boxed_i32 {
      b if b == Box::new(42) => ... // 比较整个 Box
      _ => ...
      }

结论

match 表达式中需要显式写 *boxed_i32 是因为:

  1. 模式匹配要求类型精确一致
  2. match 会消耗值的所有权
  3. 自动解引用会破坏模式匹配的语义
  4. Rust 的设计哲学要求显式处理所有权

问题_2: match 会转移所有权对吗,上面的例子怎么不会?

在 Rust 中 match 表达式通常会转移所有权,但在这个特定例子中,boxed_i32match 后仍然可用。这涉及到 Rust 的所有权系统和 Copy trait 的微妙之处。

关键点:i32 实现了 Copy trait

在上面的代码中:

1
2
3
4
5
6
let boxed_i32 = Box::new(42);
match *boxed_i32 { // 解引用 Box 得到 i32
42 => println!("It's 42!"),
_ => println!("Something else"),
}
boxed_i32; // 这里仍然可以使用!

所有权行为分析

  1. match 表达式确实会消耗值的所有权

    1
    2
    3
    4
    5
    6
    7
    8
    struct NonCopy(i32);

    let boxed = Box::new(NonCopy(42));
    match *boxed { // 消耗 NonCopy 的所有权
    NonCopy(42) => println!("OK"),
    _ => (),
    }
    // boxed; // 这里会编译错误:value used after move
  2. 问题_1例子中为什么可行?

    • i32 实现了 Copy trait
    • 当解引用 Box<i32> 时:
      1
      *boxed_i32 // 这会产生一个 i32 的副本
    • match 消耗的是这个副本,而不是原始值

具体执行过程

  1. 解引用操作

    1
    *boxed_i32 // 等价于 Deref::deref(&boxed_i32) 的返回值
    • 因为 i32Copy 类型
    • 实际产生的是原始值的位复制(bitwise copy)
  2. 模式匹配

    • match 操作的是这个复制的 i32
    • 原始 Box 的所有权保持不变
  3. 内存示意图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    栈 frame:
    +-----------------+
    | boxed_i32: Box |---+
    +-----------------+ |
    v
    堆: +-----+
    0x1000 | 42 |
    +-----+

    match *boxed_i32:
    1. 从堆复制 42 到栈临时位置
    2. 匹配临时值
    3. 原始 Box 不变

如果类型不是 Copy

使用非 Copy 类型:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
// String 不是 Copy 类型
let boxed_str = Box::new(String::from("hello"));

match *boxed_str { // 这里会移动所有权!
s if s == "hello" => println!("Got hello"),
_ => println!("Something else"),
}

// 编译错误!value borrowed here after move
// println!("{}", boxed_str);
}

错误信息:

1
2
3
4
5
6
7
8
9
10
11
error[E0382]: borrow of moved value: `boxed_str`
--> src/main.rs:10:20
|
4 | let boxed_str = Box::new(String::from("hello"));
| --------- move occurs because `boxed_str` has type `Box<String>`, which does not implement the `Copy` trait
...
7 | match *boxed_str {
| ---------- `boxed_str` moved due to this usage
...
10 | println!("{}", boxed_str);
| ^^^^^^^^^ value borrowed here after move

为什么这样设计?

  1. 性能优化

    • 对于小型的 Copy 类型(如 i32),复制成本低于间接访问
    • 避免不必要的堆分配操作
  2. 所有权语义清晰

    • 显式解引用 * 明确表示”我要获取内部值”
    • 对于 Copy 类型,这表示”我要复制值”
    • 对于非 Copy 类型,这表示”我要移动值”
  3. 一致性原则

    1
    2
    3
    4
    5
    let x = 42;
    let y = x; // 复制,x 仍然可用

    let s = String::new();
    let t = s; // 移动,s 不再可用

    match *boxed_i32 的行为与普通赋值一致

如何控制所有权行为

需求 方法 示例
保留 Box 所有权 使用 * + Copy 类型 match *boxed_i32
移动内部值 使用 * + 非 Copy 类型 match *boxed_string
借用内部值 使用 as_ref() match boxed_i32.as_ref()
匹配 Box 本身 直接使用 match boxed_i32 { ... }
部分匹配 使用 ref 关键字 match *boxed_i32 { ref val => ... }

最佳实践建议

  1. 对于 Copy 类型

    1
    2
    3
    4
    5
    // 直接解引用,安全高效
    match *boxed_num {
    42 => ...,
    _ => ...,
    }
  2. 对于非 Copy 类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 方法1:移动所有权(消耗 Box)
    match *boxed_string {
    s if s.is_empty() => ...,
    _ => ...,
    }

    // 方法2:借用内部值(保留所有权)
    match boxed_string.as_str() {
    "" => ...,
    s => ...,
    }
  3. 当需要后续使用 Box 时

    1
    2
    3
    4
    5
    6
    // 使用引用匹配
    match &*boxed_string {
    "hello" => ...,
    _ => ...,
    }
    // boxed_string 仍然可用

    所以:

    1
    2
    3
    4
    5
    6
    7
    8
    match *boxed_i32 { // 复制 i32 值
    42 => ...,
    _ => ...,
    }
    boxed_i32; // 仍然可用,因为:
    // 1. Box 的所有权从未被转移
    // 2. 解引用产生的是 i32 的副本
    // 3. 原始 Box 保持不变

这种设计体现了 Rust 的所有权系统的精妙之处:

  • 通过 Copy trait 明确区分可复制类型
  • match 的行为与赋值语义保持一致
  • 显式解引用操作符 * 清晰表达了开发者的意图
  • 编译器保证内存安全,同时提供零成本抽象

在上面的例子中,boxed_i32match 后仍然可用,正是因为 i32 实现了 Copy trait,解引用操作产生了值的副本,而不是移动所有权。

问题_3:

Deref trait 的实现:

1
2
3
4
5
6
7
impl<T: ?Sized> Deref for Box<T> {
type Target = T;

fn deref(&self) -> &T {
&**self
}
}

这个实现告诉编译器:

  • Box 可以被解引用为 &T
  • 当需要 &T 时,可以直接使用 &Box

这个不太理解,当我在使用 *Box<T> 的对象的时候,是不是意味着他会调用 deref ,然后返回 &T ?

Box<T>Deref 实现解析

1
2
3
4
5
6
7
impl<T: ?Sized> Deref for Box<T> {
type Target = T;

fn deref(&self) -> &T {
&**self
}
}

这个实现的核心是:

  1. **type Target = T**:声明解引用后的目标类型是 T
  2. **deref(&self) -> &T**:定义如何从 &Box<T> 获取 &T

关键点:* 操作符的行为

当使用 *boxed 时(其中 boxedBox<T> 类型),Rust 编译器会执行以下步骤:

  1. **隐式调用 deref**:

    1
    let tmp: &T = boxed.deref(); // 调用 Deref::deref(&boxed)
  2. 然后自动解引用

    1
    *tmp // 对返回的引用进行解引用

所以 *boxed 等价于 *(boxed.deref()),这样就解到了值 T 上。

具体示例分析

假设有:

1
let boxed = Box::new(42); // Box<i32>

场景 1:直接使用 *boxed

1
let value = *boxed; // 等价于 *(boxed.deref())

执行过程:

  1. boxed.deref() 返回 &i32(指向堆上的 42)
  2. * 解引用这个 &i32,得到 i32 值 42
  3. 因为 i32Copy 类型,值被复制到 value(因此不会转移所有权)

场景 2:方法调用中的自动解引用

1
println!("{}", boxed); // 不需要显式写 *

这里发生的是:

  1. println! 需要 &i32 作为参数
  2. 编译器发现 Box<i32> 实现了 Deref<Target = i32>
  3. 自动插入解引用:实际调用 println!("{}", &**boxed)
    • 第一个 **boxed → 通过 deref() 得到 &i32
    • 第二个 ***boxed → 解引用得到 i32
    • &&**boxed → 获取 i32 的引用

新的问题:

1
2
let boxed = Box::new(42); // Box<i32>
println!("{}", &boxed); // 不需要显式写 *

为什么写 &boxedboxed 都可以,也没有警告???

Deref trait 的实现

1
2
3
4
5
6
7
impl<T: ?Sized> Deref for Box<T> {
type Target = T;

fn deref(&self) -> &T {
&**self
}
}
  • 关键点deref() 方法接收 &self 参数,其类型是 &Box<T>
  • 返回值&T(指向内部数据的引用)

情况 1:println!("{}", &boxed)

1
2
let boxed = Box::new(42);
println!("{}", &boxed); // 显式取引用

执行过程:

  1. 类型传递

    • 传递 &boxed → 类型为 &Box<i32>
    • 这个类型 精确匹配 deref()self 参数类型
  2. deref() 调用

    1
    2
    3
    4
    // 编译器生成的等效代码
    let tmp: &i32 = Deref::deref(&boxed);
    // ^^^^^^^^^^^^^^
    // &Box<i32> 直接传递给 deref()
    • deref()self 参数接收 &Box<i32>
    • 在方法内部:&**self&(*(*self)) → 最终返回 &i32
  3. 所有权分析

    • 整个过程 没有移动所有权
    • 只是借用 boxed 并返回其内部数据的引用
    • 调用后 boxed 仍然完全有效

情况 2:println!("{}", boxed)

1
2
let boxed = Box::new(42);
println!("{}", boxed); // 直接传递值

执行过程:

  1. 类型传递

    • 传递 boxed → 类型为 Box<i32>
    • 这个类型 不匹配 deref()self 参数类型(需要 &Box<i32>
  2. 编译器插入的转换

    1
    2
    3
    4
    5
    6
    7
    8
    // 编译器生成的等效代码
    let tmp: &i32 = {
    // 第一步:创建临时引用
    let ref_to_box: &Box<i32> = &boxed;

    // 第二步:调用 deref()
    Deref::deref(ref_to_box) // 返回 &i32
    };
    • 编译器自动插入 & 操作符创建 &Box<i32>
    • 然后调用 deref(&boxed) 得到 &i32
    • println! 宏总是以引用方式处理参数
    • 即使写 boxed,宏也会自动添加 &,这点是关键,不然无法解释。
  3. 所有权分析

    • 创建了 boxed临时不可变引用
    • deref() 调用后立即释放临时引用
    • boxed 保持完整所有权
    • 调用后 boxed 仍然完全可用

关键对比表

特性 &boxed 情况 boxed 情况
传递的类型 &Box<i32> Box<i32>
匹配 deref() ✅ 直接匹配 self 参数类型 ❌ 不直接匹配
编译器转换 无转换 插入 & 创建临时引用
deref() 的 self 直接使用您的 &boxed 使用编译器创建的临时 &boxed
所有权影响 仅借用,无所有权变化 仅临时借用,无所有权变化
底层操作 deref(&boxed)&i32 deref(&(临时引用))&i32
实际机器码 完全相同的指令序列 完全相同的指令序列

为什么两种方式最终结果相同?

  1. 零成本抽象原则

    • Rust 保证两种写法生成的机器码完全相同
    • 临时引用的创建和释放在编译期优化掉
  2. 自动解引用规则

    1
    2
    T: Deref<Target = U>
    ⇒ &T 自动转换为 &U
    • 两种方式都通过这个规则转换为 &i32
  3. println! 的隐式行为

    • println! 宏总是以引用方式处理参数
    • 即使写 boxed,宏也会自动添加 &

实际代码验证

我们可以用显式类型标注来验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let boxed = Box::new(42);

// 情况 1:显式取引用
let ref1: &Box<i32> = &boxed;
let deref_result1: &i32 = ref1.deref();

// 情况 2:直接传递值
let ref2: &Box<i32> = &boxed; // 编译器自动添加这步
let deref_result2: &i32 = ref2.deref();

// 两种方式获得相同的引用
assert_eq!(deref_result1 as *const i32, deref_result2 as *const i32);
println!("地址相同:{:p} == {:p}", deref_result1, deref_result2);
}

输出结果:

1
地址相同:0x7f8a6bc05d04 == 0x7f8a6bc05d04

结论

  1. **当您使用 &boxed**:

    • 直接传递 &Box<i32>deref()
    • 精确匹配方法签名,无需额外转换
  2. **当您使用 boxed**:

    • 编译器自动插入 & 创建临时引用 &Box<i32>
    • 然后调用 deref() 获取 &i32
    • 临时引用在表达式结束后立即释放
  3. 本质相同

    • 两种方式最终都执行 deref(&boxed)
    • 生成完全相同的机器指令
    • 都只涉及借用,不涉及所有权转移

这就是 Rust 的自动解引用(Deref Coercion)机制的精妙之处——它让智能指针的使用感觉像直接操作底层数据,同时保持绝对的类型安全和零运行时开销。

为什么设计成这样?

这种设计实现了:

  1. 统一访问语法:无论访问栈数据还是堆数据,都使用相同的 * 语法
  2. 零成本抽象:最终生成的机器码与直接访问内存相同
  3. 类型安全:通过 trait 系统保证解引用操作的安全

&**self 的魔法

在实现中的 &**self 需要特别解释:

1
2
3
fn deref(&self) -> &T {
&**self
}

分解步骤:

  1. self&Box<T>
  2. *self:解引用得到 Box<T>(移动语义)
  3. **self:对 Box<T> 解引用得到 T(通过编译器内置行为)
  4. &**self:获取 T 的引用

实际上,Box<T> 的解引用到 T 是编译器内置操作,deref() 只是暴露这个能力给 trait 系统。

所有权影响

*boxed 的所有权行为取决于 T

  • 如果 T 实现了 Copy(如 i32):值被复制,原始 Box 不受影响
  • 如果 T 未实现 Copy(如 String):值被移动,Box 变为空壳(不能再使用)

示例:

1
2
3
4
5
6
7
8
9
// 实现 Copy 的类型
let boxed_i32 = Box::new(42);
let value = *boxed_i32; // 复制
println!("{}", boxed_i32); // 仍然可用

// 未实现 Copy 的类型
let boxed_str = Box::new(String::from("hello"));
let s = *boxed_str; // 移动所有权
// println!("{}", boxed_str); // 错误!值已被移动

自动解引用(Deref Coercion)规则

当需要 &T 而您有 &Box<T> 时,编译器会自动插入解引用链:

1
2
3
4
5
6
fn print_num(n: &i32) {
println!("{}", n);
}

let boxed = Box::new(42);
print_num(&boxed); // 自动转换为 &(*boxed)

转换过程:

  1. &boxed&Box<i32>
  2. 因为 Box<i32>: Deref<Target = i32>
  3. 编译器插入 Deref::deref(&boxed)&i32
  4. 完美匹配函数签名

总结

  1. *boxed 会:

    • 调用 deref() 获取 &T
    • 然后解引用该引用获取 T
  2. 当需要 &T 时:

    • 可以直接使用 &boxed,编译器自动调用 deref()
    • 等价于显式写 &**boxed
  3. Box<T>deref() 实现:

    1
    &**self // 意思是:先解引用 Box,然后获取内部值的引用
  4. 这种设计使得智能指针在使用上几乎与普通引用无区别,同时保持了类型安全和零成本抽象。


Rust:Box、match、ownership
http://blog.luliang.online/2025/09/27/Rust(五)/
作者
Luyoung
发布于
2025年9月27日
许可协议