【边刷Leetcode边学Rust】链表的定义(上)——指针Box<T>

315 阅读3分钟

【边刷Leetcode边学Rust】链表的定义(上)——指针Box

今天我们来看一下如何用Rust定义链表。以Leetcode中的链表题为例,链表的定义通常是

// Definition for singly-linked list.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
    pub val: i32,
    pub next: Option<Box<ListNode>>
}

next字段(成员)的类型Option<Box<ListNode>>恐怕是定义中最不好理解的部分了。下面我们就由内向外,一层层分析这个类型。

递归的类型定义

给定链表中的一个节点,类型为ListNode,由于该节点的后面要么是另一个类型同为ListNode节点,要么是“空”,所以next字段自然是指向后续节点的指针(或对后续节点的引用)。

从形式上来看,next字段的类型中又出现了结构体的名字或类名,即在类型T的定义中又用到了类型T本身。Java语言在这一点上较为直观,若使用Java,可以将链表定义为

public class ListNode {
    int val;
    ListNode next;
    
    // constructors
    // ListNode() {}
    // ...
}

那在Rust中为什么不能这样(在类型T中引用T本身)定义链表呢?

pub struct ListNode {
    pub val: i32,
    pub next: ListNode
}

最直接的原因是,在ListNode直接引用ListNode无法通过编译:

error[E0072]: recursive type `ListNode` has infinite size
 --> src/main.rs:2:1
  |
2 | pub struct ListNode {
  | ^^^^^^^^^^^^^^^^^^^
3 |     pub val: i32,
4 |     pub next: ListNode
  |               -------- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
4 |     pub next: Box<ListNode>
  |               ++++        +
​

其实,不仅是Rust,C、C++、Go等语言也无法编译这样的代码,以Go为例:

type ListNode struct {
  Val  int
  Next ListNode // ⚠️注意,这里不是`*ListNode`
}
// ./prog.go:7:6: invalid recursive type: ListNode refers to itself

这背后的原因在于编译器无法计算递归类型所需的空间大小

【边刷Leetcode边学Rust】链表的定义-1-listnode.png

要想知道ListNode类型所需的空间就必须先知道ListNode类型所需的空间,这样无穷无尽递归下去,编译器就就只好返回错误“recursive type ListNode has infinite size”了。

指针Box

Rust的编译器除了抱怨递归类型无法计算大小以外,还提示我们

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
4 |     pub next: Box<ListNode>
  |               ++++        +

于是我们见到了Option<Box<ListNode>>中夹在中间的Box<ListNode>

Box<T>能够在堆上存储数据,例如

// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7b2784a91411ae6958251b688e1f00fb

use std::ptr;

fn main() {
    let x = Box::<i32>::new(42);
    println!("x = {}({:p})", x, x);
    println!("x @ {:?}", ptr::addr_of!(x));
    
    let ptr_x = Box::into_raw(x);
    println!("{:?}", unsafe {*(ptr_x)})

}

这段代码将会输出如下内容,从中可以看出:

  • x = 42(0x55c8fe9e49d0)

    • 42是我们通过Box::new()上创建的一个整数
    • x是存储在上的指针,类型为Box<i32>,指向上的42
    • 0x55c8fe9e49d0是存储在变量x中的地址,这个地址正是在上的42的地址
    • 无须使用*x即可访问x指向的数据
  • x @ 0x7ffcb07cea60

    • 通过宏ptr::addr_of!()可以得到x的(在上的)地址
  • 42

    • Box::into_raw()可将x转换为原始指针(raw pointer)ptr_x
    • 通过unsafe {*(ptr_x)}解引用这个原始指针,可以得到存储在上的42

Box允许创建递归类型

由于Box<T>是指针,而指针有已知的大小,所以通过在递归的类型定义中插入Box,就可以创建递归的类型了。这也就是为什么在C、C++、Go等语言中,定义链表时,要将next字段的类型定义为ListNode*ListNode&*ListNode等了(注意*&)。

套上Box以后,编译器不再报错了^_^

pub struct ListNode {
    pub val: i32,
    pub next: Box<ListNode>
}

既然Box<ListNode>已经通过了编译,为什么还要再套上一层Option呢?在Rust中如何表示空值(null、nil)呢?且听下回分解。