【边刷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
这背后的原因在于编译器无法计算递归类型所需的空间大小。
要想知道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>,指向堆上的420x55c8fe9e49d0是存储在变量x中的地址,这个地址正是在堆上的42的地址- 无须使用
*x即可访问x指向的数据
-
x @ 0x7ffcb07cea60- 通过宏
ptr::addr_of!()可以得到x的(在栈上的)地址
- 通过宏
-
42Box::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)呢?且听下回分解。