【边刷Leetcode边学Rust】链表的定义(下)——标准库中的枚举类型Option

90 阅读5分钟

【边刷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),
}

NoneSome(T)都是Option<T>的成员(variants)。对于给定的类型TOption<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;
    }
}