首页
/ Rustlings Options 练习解析:用 Option 表达"值可能不存在"的 Rust 惯用方式

Rustlings Options 练习解析:用 Option 表达"值可能不存在"的 Rust 惯用方式

2026-09-04 14:20:27作者:明树来

Option<T> 是 Rust 标准库中最核心的类型之一,它用 Some(T)None 两个变体在类型系统层面强制开发者处理"值可能不存在"的情况。本篇以 rustlings 仓库中的 exercises/12_options 练习组为主体,完整讲解 Option 的类型定义、八大典型用途,以及三道练习(返回 Option 的偏函数、if let/while let 模式匹配、match 与移动语义)的完整解法,并结合 rustlings 工具自身源码中的真实 Option 用例,说明该模式在工程实践中的落地方式。读完本文,你将掌握 Option 的各种解构手法,并能在自己的 Rust 代码中自信地使用它替代空指针。

Option 类型:Either Some 或 None

Option 是一个枚举类型,代表一个可选的值:每个 Option 要么是包含值的 Some,要么是不含值的 None。其标准库定义为:

pub enum Option<T> {
    Some(T),
    None,
}

正如 exercises/12_options/README.md 所述,Option 类型在 Rust 代码中非常常见,因为它承担了许多职责:

  • 初始值(Initial values):变量尚未被赋予真实值时,用 None 占位;
  • 偏函数的返回值(Partial functions):函数在其输入域的某些取值上没有定义(例如字符串转整数失败、数组越界查找),用 Option 表示"可能无结果";
  • 简单错误报告:在没有引入完整错误类型(Result)之前,用 None 表示失败;
  • 可选的 struct 字段:字段可能缺失,如 dir: Option<String>
  • 可被借用或"取走"(taken)的 struct 字段:配合 Option::take() 把值临时移出;
  • 可选的函数参数:参数可缺省时用 Option 包装;
  • 空指针(Nullable pointers):Rust 中 None 就是没有空指针解引用风险的 null;
  • 从棘手情境中把东西"换出"(Swapping things out):利用所有权规则安全地交换数据。

这一节列出的用途在后面的练习和 rustlings 源码中都会一一对应出现。

练习一:options1 —— 用 Option 表达偏函数

exercises/12_options/options1.rs 的第一题要求实现一个函数:

// This function returns how much ice cream there is left in the fridge.
// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00,
// someone eats it all, so no ice cream is left (value 0). Return `None` if
// `hour_of_day` is higher than 23.
fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
    // TODO: Complete the function body.
}

这正对应 README 中"偏函数"这一用途:函数语义是"给定一天中的小时,返回冰箱里剩多少冰淇淋",但输入 24 及以上根本没有定义。Rust 不允许返回空值,所以返回类型是 Option<u16> 而不是裸的 u16。官方解法在 solutions/12_options/options1.rs

fn maybe_ice_cream(hour_of_day: u16) -> Option<u16> {
    match hour_of_day {
        0..=21 => Some(5),
        22..=23 => Some(0),
        _ => None,
    }
}

要点说明:

  • 0..=2122..=23闭区间 range 模式,在 match 中直接匹配值域,比写 if/else if 更声明化;
  • _ => None 兜底所有超出 0–23 的输入,把"无定义"显式编码进返回值;
  • 测试里验证了三段语义:maybe_ice_cream(0/9/18) 返回 Some(5)maybe_ice_cream(22/23) 返回 Some(0)maybe_ice_cream(24/25) 返回 None

另一个细节在测试 raw_value 中。原始题目给出的断言 assert_eq!(ice_creams, 5) 无法编译,因为 ice_creamsOption<u16>5u16,两者类型不同。解法(solutions/12_options/options1.rs 第 22–27 行)是:

// Using `unwrap` is fine in a test.
let ice_creams = maybe_ice_cream(12).unwrap();

assert_eq!(ice_creams, 5);

unwrap()Some(v) 中取出 v,遇到 None 则 panic。在测试代码里使用是合理的——因为测试本身就是"我确定这里是 Some,否则说明实现有 bug"的断言。若在业务代码中随意 unwrap(),则失去了 Option 的保护意义;更稳妥的替代是 expect("消息")(panic 时给出上下文)或 unwrap_or(默认值)unwrap_or_else(|| fallback)

练习二:options2 —— if let 与 while let 解构 Option

exercises/12_options/options2.rs 的第二题训练 if letwhile let 这两个"专门针对单一模式的匹配语法糖"。题目故意删掉了关键部分,留下两个不完整的语句:

// 题目(不完整,无法编译)
word = optional_target {
    assert_eq!(word, target);
}
integer = optional_integers.pop() {
    assert_eq!(integer, cursor);
    cursor -= 1;
}

官方解法在 solutions/12_options/options2.rs

简单情况:if let 匹配 Some

#[test]
fn simple_option() {
    let target = "rustlings";
    let optional_target = Some(target);

    // if-let
    if let Some(word) = optional_target {
        assert_eq!(word, target);
    }
}

if let Some(word) = optional_target { ... } 等价于 match optional_target { Some(word) => { ... } _ => {} },但更简洁。注意 if let 分支执行后 optional_target 已被移动(moved)——Some 分支里的模式 Some(word) 会拿走内部值,因此之后不能再使用 optional_target

嵌套情况:while let + 双层 Option

#[test]
fn layered_option() {
    let range = 10;
    let mut optional_integers: Vec<Option<i8>> = vec![None];

    for i in 1..=range {
        optional_integers.push(Some(i));
    }

    let mut cursor = range;

    // while-let with nested pattern matching
    while let Some(Some(integer)) = optional_integers.pop() {
        assert_eq!(integer, cursor);
        cursor -= 1;
    }

    assert_eq!(cursor, 0);
}

这里的精髓是"两层 Option"的嵌套模式匹配:

  1. Vec::pop() 的返回类型是 Option<T>——向量可能已空,返回 None
  2. 而本例中 T 本身是 Option<i8>,所以 pop() 实际返回 Option<Option<i8>>
  3. 模式 Some(Some(integer)) 一次性剥掉两层:外层 Some 保证"栈非空",内层 Some 保证"元素本身有值";
  4. while let 在该模式不匹配时(pop() 得到 None,即 vec![None] 初始元素之后的空向量,或取出尾部那个 None 元素时)退出循环。

具体地,向量初始为 [None, Some(1), ..., Some(10)]:循环先从尾部依次弹出 Some(10) 直到 Some(1)cursor 递减到 0;再弹出最底部的 None 元素时,pop() 返回 Some(None),与 Some(Some(integer)) 不匹配,循环结束。这正是题目注释所说的"Remember that Vec::pop() adds another layer of Option"。

if let / while let 在 rustlings 工具本体的源码中被大量使用,例如 src/app_state.rsif let Some(ind) = self.next_pending_exercise_ind()(第 510 行)、src/cargo_toml.rsif let Some(dir) = exercise_info.dir(第 41 行),说明这是处理"可能不存在的值"的日常首选写法。

练习三:options3 —— match Option 结构体与移动语义

exercises/12_options/options3.rs 的第三题:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let optional_point = Some(Point { x: 100, y: 200 });

    // TODO: Fix the compiler error by adding something to this match statement.
    match optional_point {
        Some(p) => println!("Coordinates are {},{}", p.x, p.y),
        _ => panic!("No match!"),
    }

    println!("{optional_point:?}"); // Don't change this line.
}

矛盾点在于:模式 Some(p) 会把 Point optional_point 中移动出来,而函数末尾那行 println!("{optional_point:?}") 仍要求 optional_point 存在且实现了 Debug(所以结构体上有 #[derive(Debug)])。移动之后再用,编译器直接报错。官方解法在 solutions/12_options/options3.rs,给出两种等价方案:

方案一:模式中使用 ref,按引用绑定。

match optional_point {
    Some(ref p) => println!("Coordinates are {},{}", p.x, p.y),
    //   ^^^ added
    _ => panic!("No match!"),
}

ref p 表示"把 p 绑定为 &Point 而不是移走 Point"。match 结束后 optional_point 依然完整可用。

方案二:对引用进行 match,匹配对象整体变成 &Option<Point>

match &optional_point {
    //^ added
    Some(p) => println!("Coordinates are {},{}", p.x, p.y),
    _ => panic!("No match!"),
}

匹配 &optional_point 时,模式会自动"解引用穿透"引用:Some(p) 实际匹配的是 &Some(...),此时 p 被推断为 &Point,同样没有发生移动。这两种方案都利用了 Rust 的匹配借用规则(match ergonomics),是处理"我想读一下 Option 里的值但不想拿走它"的标准做法。

这个练习实际上把 Option 练习和所有权练习打通了:解构 Option 是否触发移动,完全取决于你绑定的是值还是引用。

从 rustlings 源码看 Option 的工程化用法

三道练习覆盖了"构造 Option"、"if let/while let 解构"、"match 与借用"三条主线。rustlings 工具自身的源码提供了更多真实工程佐证,以下每个用例都能对应回 README 中列出的用途。

用途"偏函数返回值":src/dev/check.rs

// Find a char that isn't allowed in the exercise's `name` or `dir`.
fn forbidden_char(input: &str) -> Option<char> {
    input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
}

Iterator::find 找不到元素时返回 None,天然适合"可能无结果"的查询。调用方在 src/dev/check.rsif let Some(c) = forbidden_char(name) 只关心"找到违例字符"的分支,找到就 bail! 报错,没找到则静默通过——这就是 Option 控制流的典型形态。

用途"简单错误报告":src/term.rs

/// Canonicalize, convert to string and remove verbatim part on Windows.
pub fn canonicalize(path: &str) -> Option<String> {
    fs::canonicalize(path)
        .ok()?
        .into_os_string()
        .into_string()
        .ok()
        .map(|mut path| {
            // Windows itself can't handle its verbatim paths.
            if cfg!(windows) && path.as_bytes().starts_with(br"\\?\") {
                path.drain(..4);
            }
            path
        })
}

这段代码浓缩了 Option 的几个核心方法:

  • .ok()?:把 Result<T, E> 转成 Option<T>OkSomeErrNone),? 操作符在 None 时直接从当前函数返回——用 Option 承载错误时,? 同样适用;
  • .map(|...|):在 Some 内对值做转换(这里是剥掉 Windows 的 \\?\ verbatim 前缀);
  • 函数签名 -> Option<String> 把"路径规范化可能失败"这一事实暴露在类型上,调用方必须显式处理 None

用途"初始值/索引可能不存在":src/app_state.rs

// Return the index of the next pending exercise or `None` if all exercises are done.
fn next_pending_exercise_ind(&self) -> Option<usize> {
    let next_ind = self.current_exercise_ind + 1;
    self.exercises
        // If the exercise done isn't the last, search for pending exercises after it.
        .get(next_ind..)
        .and_then(|later_exercises| {
            later_exercises
                .iter()
                .position(|exercise| !exercise.done)
                .map(|ind| next_ind + ind)
        })
        // Search from the start.
        .or_else(|| { ... })
}

这里演示了 Option 的组合子(combinators)链:

  • .get(next_ind..):越界切片返回 None 而非 panic;
  • .and_then(...):仅当为 Some 时执行闭包并展开其结果,否则短路保留 None
  • .position(...):迭代器查找,找不到返回 None
  • .or_else(...):第一个查找落空时回退到"从头再找"的备选逻辑。

这种 Option 链式调用避免了层层嵌套的 if let ... { if let ... { ... } },是处理"多级可能缺失"的惯用写法。类似的还有 src/editor/zellij.rsparse_pane_id(b: &[u8]) -> Option<(String, u32)>——解析外部工具输出,失败就返回 None

小结:Option 的解构工具箱

回到 exercises/12_options/README.md 的主题,可以把本篇的练习与源码用例归纳为一张速查表:

需求 手段 出处
Option<T> 中取值(确定非 None .unwrap() / .expect(msg) solutions/12_options/options1.rs
只关心 Some 分支 if let Some(x) = opt { ... } solutions/12_options/options2.rs
循环直到 Option 变空 while let Some(x) = iter.next() { ... } solutions/12_options/options2.rs
嵌套 Option 匹配 Some(Some(inner)) 模式 solutions/12_options/options2.rs
只借用不移动 Some(ref p)match &opt solutions/12_options/options3.rs
ResultOption 并短路 .ok()? src/term.rs
链式变换与回退 .map() / .and_then() / .or_else() src/app_state.rs
表达"输入域外无定义" 函数返回 Option<T>match 给出 None 分支 solutions/12_options/options1.rs

Option 的价值不在"判空"本身,而在于它把"值可能不存在"写进了类型:编译器会一路追着你,直到你显式决定 None 时怎么办。完成这三道练习后,你可以继续做 exercises/13_error_handling,那里会介绍 Result<T, E>——与 Option 结构相同、但携带具体错误信息的"错误版 Option",两者常配合 ? 操作符一起使用。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384