【边刷Leetcode边学Rust】链表的定义(下)——标准库中的枚举类型Option
通过上一篇文章(【边刷Leetcode边学Rust】链表的定义(上)——指针Box<T>),我们了解了在Leetcode给出的链表定义中
// Definition for singly-linked list.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
pub val: i32,
pub next: Option<Box<ListNode>>
}
next
字段的类型为什么不能直接写作pub next: ListNode
,以及Box::new()
的作用。今天我们再来看看为什么还要将Box<ListNode>
套在Option<>
中。
标准库中的枚举类型Option
next
字段表示链表中某个节点之后节点,下一个节点要么存在,要么不存在,只有这两种可能。也就是说,我们可以枚举出next
的所有可能的取值,next
要么是Box<ListNode>
的值,表示下一个节点的地址;要么是——借用其他语言中的概念——空指针(NULL, nil等),表示后面没有节点了。
这种某个类型的值是否存在的场景在代码中极其普遍,所以Rust的标准库提供了名为Option
的枚举类型来反映这种情况。Option
的定义如下
enum Option<T> {
None,
Some(T),
}
None
和Some(T)
都是Option<T>
的成员(variants)。对于给定的类型T
,Option<T>
类型的值要么是Some(v)
(是的这个看似函数调用的表达式是一个值),其中v
是类型T
的值;要么是None
,表示不存在类型T
的值,姑且可以看作其他语言中的null, nil等。
下面先来看一看Some(T)
这个成员。
将数据(值)直接放进枚举成员中
在有些语言中,枚举类型有时只不过是一系列常量的列表,如在Java中,
// 参考:https://www.geeksforgeeks.org/enum-in-java/
enum Color {
RED,
GREEN,
BLUE;
}
public class Test {
public static void main(String[] args)
{
Color c1 = Color.RED;
System.out.println(c1); // Output: RED
}
}
在Rust中也可以这样定义枚举:
// 参考:https://rustwiki.org/zh-CN/book/ch06-01-defining-an-enum.html
// IP地址有v4和v6两个版本
enum IpAddrKind {
V4,
V6,
}
在这个枚举的基础上,我们可以定义表示IP地址的结构体IpAddr
:
// 参考:https://rustwiki.org/zh-CN/book/ch06-01-defining-an-enum.html
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
现在,我们是通过结构体将枚举的成员(如IpAddrKind::V4
)和对应的数据(如"127.0.0.1"
)结合在一起的,而Rust提供了更加简洁的表示方法,
// 参考:https://rustwiki.org/zh-CN/book/ch06-01-defining-an-enum.html
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
即直接将数据附加到枚举的成员上,这样就不需要额外的结构体了。Option
中的Some(T)
也是如此,类型T
的值v
直接附加在Some
成员上,即Some(v)
,下面来看一看如何获取附加在Some
成员上的值。
获取附加在Some(T)
成员上的值
下面的代码创建了一个仅含有2个节点的链表,由此可以看出,
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
pub val: i32,
pub next: Option<Box<ListNode>>,
}
fn main() {
// list: 1 -> 2 -> null
let node2 = Some(Box::new(ListNode{val: 2, next: None}));
let node1 = Some(Box::new(ListNode{val: 1, next: node2}));
println!("{:?}", node1.unwrap());
// Output: ListNode { val: 1, next: Some(ListNode { val: 2, next: None }) }
}
- 可以直接使用
None
来创建表示值不存在的枚举值 - 可以使用
Some(v)
来创建带有值v
的枚举值,表示某类型的值存在 - 可以通过
Option<T>
上的unwrap()
方法获取附加在Some
成员上的值
但当尝试对第二个节点(即链表中最后一个节点)的next
调用unwrap()
时,
fn main() {
// list: 1 -> 2 -> null
let node2 = Some(Box::new(ListNode { val: 2, next: None }));
println!("{:?}", node2.unwrap().next.unwrap());
}
编译器会报错
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:10:42
也就是说,当Option<T>
的值为None
时,会产生panic。为了避免panic,可以先使用is_none()
判断是否为None
值,或者调用unwrap_or*()
系列的函数。
Option枚举类型相对于空值的优势
至此,应该大概能明白Leetcode中为什么将next
字段的类型定义为Option<Box<ListNode>>
了吧。
ListNode
:如果有下一个节点,那么其类型与当前节点的类型ListNode
相同Box<ListNode>
:指针类型的大小是已知的,避免编译器无法计算递归类型的大小Option<Box<ListNode>>
:当前节点要么有下一个节点Some(Box<ListNode>)
,即通过指针指向下一个节点;要么是链表中的最后一个节点,即没有(None
)指向下一个节点指针。
怎么样,似乎也没有多复杂吧,已经跃跃欲试,准备开始用Rust刷链表的题目了吗?在此之前,我们再来看一下Option<T>
相对于其他语言中空值(null或nil等)的优势。
Option<T>
避免了空指针异常,因为Option引入了新类型,编译器能够确保定义在类型T
上的方法不能应用于Option<T>
。要想调用类型T
上的方法,程序员就必须处理Option<T>
,无论是判断is_none()
,还是unwrap*()
,都明确地处理了空值的情况。在Rust中,只要一个值不是 Option<T>
类型,就可以认定它不是空值。
关于主流语言如何表示空值,以及表示方法的优劣,可以参考:
赶紧来刷一道简单题
赶紧来刷一道链表题试一试吧,比如这道题剑指 Offer 06. 从尾到头打印链表。消除了对Option<Box<ListNode>>
的迷惑后,不就是个链表遍历吗,轻轻松松就写出了如下代码(这才是真正受挫的开始)。可这段代码为什么连编译都不能通过呢,又该如何修改呢?
// 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
impl Solution {
pub fn reverse_print(head: Option<Box<ListNode>>) -> Vec<i32> {
let mut ans = vec![];
let mut p = head;
while p != None {
ans.push(p.unwrap().val);
p = p.unwrap().next;
}
ans.reverse();
return ans;
}
}