ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Rust By Example 实战:Option 与 unwrap——用 match、expect 与 `?` 优雅处理“可能没有值“的 Rust 可选值编程

Rust By Example 实战:Option 与 unwrap——用 match、expect 与 `?` 优雅处理“可能没有值“的 Rust 可选值编程 文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载Rust 是一门强调可失败性必须显式建模的系统语言而标准库中的OptionT枚举正是处理某个值可能存在、也可能不存在这一场景的基石。本文以 Rust By Example 官方仓库的 Option unwrap 章节 为主体完整讲解Option的两个变体、match显式分支与unwrap隐式解包的取舍并沿其子章节纵深延伸到?运算符、map/and_then组合器与默认值回退四件套让你在阅读完本文后能够为任何可选值场景写出既安全又简洁的 Rust 代码。从可控崩溃到可建模缺失为什么需要Option在 Rust By Example 的错误处理脉络中最先登场的是panic当程序碰到不可恢复的错误条件时可以主动调用panic!打印错误信息、开始栈展开unwinding并通常终止程序。例如经典的喝到柠檬水就崩溃示例fn drink(beverage: str) { // You shouldnt drink too many sugary beverages. if beverage lemonade { panic!(AAAaaaaa!!!!); } println!(Some refreshing {} is all I need., beverage); } fn main() { drink(water); drink(lemonade); drink(still water); }第一次调用drink(water)正常执行第二次调用drink(lemonade)触发panic第三次调用永远不会执行。但panic只解决了值存在但非法的问题。原文档提出一个更微妙的场景如果程序期待某种饮料却压根没有收到任何饮料该怎么办理论上可以用空字符串来探测但在 Rust 中更符合语言精神的做法是——让编译器替你指出没有饮料的那些路径。这正是OptionT存在的意义把缺失从运行时约定提升为编译期类型信息让类型系统强制你处理缺失的可能性。从错误处理的整体策略看见 error.md 的规则总结显式panic主要用于测试和不可恢复错误原型阶段可用unimplemented!标记未实现函数当值缺失不是错误条件例如根目录/没有父目录时使用Option当确实可能出错且调用方需要处理时使用Result。OptionT标准库中可能缺失的建模方式std库中的enum OptionT用于表示缺失是可能发生的情况它只有两个变体详见 std/option.mdSome(T)找到了一个类型为T的元素None没有找到任何元素。一个最直观的实践是不做除零 panic 的整数除法// An integer division that doesnt panic! fn checked_division(dividend: i32, divisor: i32) - Optioni32 { if divisor 0 { // Failure is represented as the None variant None } else { // Result is wrapped in a Some variant Some(dividend / divisor) } } // This function handles a division that may not succeed fn try_division(dividend: i32, divisor: i32) { // Option values can be pattern matched, just like other enums match checked_division(dividend, divisor) { None println!({} / {} failed!, dividend, divisor), Some(quotient) { println!({} / {} {}, dividend, divisor, quotient) }, } } fn main() { try_division(4, 2); try_division(1, 0); // Binding None to a variable needs to be type annotated let none: Optioni32 None; let _equivalent_none None::i32; let optional_float Some(0f32); // Unwrapping a Some variant will extract the value wrapped. println!({:?} unwraps to {:?}, optional_float, optional_float.unwrap()); // Unwrapping a None variant will panic! println!({:?} unwraps to {:?}, none, none.unwrap()); }这个示例还揭示了两个容易被忽视的细节None没有携带类型信息绑定到变量时必须做类型标注let none: Optioni32 None;也可以用 turbofish 语法None::i32显式指定unwrap一个Some会取出内层值unwrap一个None则直接panic!——这就是隐式处理的代价。显式 vs 隐式match与unwrap的取舍原文档option_unwrap.md用一句话点破了两种处理方式的本质Option的两种情况既可以通过match显式处理也可以通过unwrap隐式处理——隐式处理要么返回内层元素要么panic。unwrap在遇到None时确实会崩溃但默认的 panic 输出信息比较含糊called Option::unwrap() on a None value。可以通过expect手动定制 panic 消息expect与unwrap行为相同但在None时输出你提供的自定义错误文案便于定位问题。即便如此unwrap的产物仍然不如显式处理那样可控——显式match可以在想要 panic和想要优雅兜底之间自由选择。核心示例精讲成人与孩子的两种喝法原文档给出了一个极具代表性的对照实验——同一份Optionstr数据give_adult用match显式处理所有分支drink则用unwrap隐式解包// The adult has seen it all, and can handle any drink well. // All drinks are handled explicitly using match. fn give_adult(drink: Optionstr) { // Specify a course of action for each case. match drink { Some(lemonade) println!(Yuck! Too sugary.), Some(inner) println!({}? How nice., inner), None println!(No drink? Oh well.), } } // Others will panic before drinking sugary drinks. // All drinks are handled implicitly using unwrap. fn drink(drink: Optionstr) { // unwrap returns a panic when it receives a None. let inside drink.unwrap(); if inside lemonade { panic!(AAAaaaaa!!!!); } println!(I love {}s!!!!!, inside); } fn main() { let water Some(water); let lemonade Some(lemonade); let void None; give_adult(water); give_adult(lemonade); give_adult(void); let coffee Some(coffee); let nothing None; drink(coffee); drink(nothing); }逐行拆解这个示例可以看到三条关键设计match的守卫顺序give_adult中Some(lemonade)是字面量模式必须先于通配的Some(inner)出现否则柠檬水分支永远无法命中None分支则处理没有饮料的情形程序全程不会崩溃。unwrap的单线解包drink中let inside drink.unwrap();直接取出str逻辑上等价于要么给我值要么让我 panic。当传入None时unwrap触发 panicif inside lemonade这行根本不会执行。面向不同调用方的心智模型give_adult展示了每条路径都有明确处置的生产级写法drink展示了原型阶段快速取值、交给后续逻辑校验的简化写法——代价是None会以最粗暴的方式终结程序。运行时行为main中前三行依次打印water? How nice.、Yuck! Too sugary.、No drink? Oh well.drink(coffee)打印I love coffees!!!!!而drink(nothing)直接 panic 终止程序。选择指南panic/unwrap/expect/match何时登场结合 error.md 给出的经验法则可以形成一张清晰的决策表场景推荐手段理由测试中断言某条件必然成立panic!或assert!系列测试中显式失败正是目的原型阶段调用尚未实现的函数unimplemented!/todo!语义比panic!更清晰值缺失不是错误如目录父级Optionmatch建模可选而非出错原型或 100% 确信存在值unwrap快速取值确信存在值但想留下可诊断信息expect(message)崩溃时输出自定义信息存在合理缺失路径需要分支处理match/if let全路径可控可能出错且调用方要处理Result?错误传播而非崩溃一句话记忆能用类型表达缺失就不用 panic能用?传播就不用 unwrap能用组合器链式处理就不用嵌套 match。优雅解包升级?运算符让嵌套match消失match能解包Option但层层嵌套很快就变得啰嗦。?运算符提供了一条捷径若x是Option求值x?时——若x是Some返回内层值若x是None立即终止当前函数并返回None。以生日计算为例见 question_mark.mdfn next_birthday(current_age: Optionu8) - OptionString { // If current_age is None, this returns None. // If current_age is Some, the inner u8 value 1 // gets assigned to next_age let next_age: u8 current_age? 1; Some(format!(Next year I will be {}, next_age)) }?的威力在深层嵌套数据结构中体现得淋漓尽致——取某人的工作电话号码的区号如果不用?需要写大量嵌套match用了?后一条链搞定struct Person { job: OptionJob, } #[derive(Clone, Copy)] struct Job { phone_number: OptionPhoneNumber, } #[derive(Clone, Copy)] #[allow(dead_code)] struct PhoneNumber { area_code: Optionu8, number: u32, } impl Person { // Gets the area code of the phone number of the persons job, if it exists. fn work_phone_area_code(self) - Optionu8 { // This would need many nested match statements without the ? operator. // It would take a lot more code - try writing it yourself and see which // is easier. self.job?.phone_number?.area_code } } fn main() { let p Person { job: Some(Job { phone_number: Some(PhoneNumber { area_code: Some(61), number: 439222222, }), }), }; assert_eq!(p.work_phone_area_code(), Some(61)); }self.job?.phone_number?.area_code三连?任何一环是None函数就整体返回None全部是Some才一路解包到area_code。这正是链式解包的教科书写法。Rust By Example 还提示?之于Result的版本遇到Err时return而非panic详见 Introducing?那里还回顾了历史遗留的try!宏。组合器Combinatormap让变换免去样板 match显式match虽然安全但频繁书写显得冗长。Option内置的map()是一个组合器combinator对Some - Some做映射对None - None原样透传且支持链式调用。原文档用备餐流水线展示了map如何压缩代码见 map.md#![allow(dead_code)] #[derive(Debug)] enum Food { Apple, Carrot, Potato } #[derive(Debug)] struct Peeled(Food); #[derive(Debug)] struct Chopped(Food); #[derive(Debug)] struct Cooked(Food); // Peeling food. If there isnt any, then return None. // Otherwise, return the peeled food. fn peel(food: OptionFood) - OptionPeeled { match food { Some(food) Some(Peeled(food)), None None, } } // Chopping food. If there isnt any, then return None. // Otherwise, return the chopped food. fn chop(peeled: OptionPeeled) - OptionChopped { match peeled { Some(Peeled(food)) Some(Chopped(food)), None None, } } // Cooking food. Here, we showcase map() instead of match for case handling. fn cook(chopped: OptionChopped) - OptionCooked { chopped.map(|Chopped(food)| Cooked(food)) } // A function to peel, chop, and cook food all in sequence. // We chain multiple uses of map() to simplify the code. fn process(food: OptionFood) - OptionCooked { food.map(|f| Peeled(f)) .map(|Peeled(f)| Chopped(f)) .map(|Chopped(f)| Cooked(f)) } // Check whether theres food or not before trying to eat it! fn eat(food: OptionCooked) { match food { Some(food) println!(Mmm. I love {:?}, food), None println!(Oh no! It wasnt edible.), } } fn main() { let apple Some(Food::Apple); let carrot Some(Food::Carrot); let potato None; let cooked_apple cook(chop(peel(apple))); let cooked_carrot cook(chop(peel(carrot))); // Lets try the simpler looking process() now. let cooked_potato process(potato); eat(cooked_apple); eat(cooked_carrot); eat(cooked_potato); }对比peel/chop的手写match与cook/process的map版本可以直观看到match版本每个函数都要重复Some则变换、None则透传的样板而map版本一行搞定且process把三次变换压缩成一条链。运行时Some链上的每一步都会执行映射而potatoNone从头到尾保持None最终打印Oh no! It wasnt edible.。组合器进阶and_thenflatmap避免OptionOptionT嵌套map很优雅但有个陷阱当映射函数本身返回OptionT时map会得到OptionOptionT链式调用会越来越混乱。此时需要and_then()其他语言中常称 flatmap它以被包裹的值调用传入函数并直接返回该函数的结果若原Option为None则直接返回None不再嵌套见 and_then.md。原文档用烹饪可行性的三种写法展示问题与解法#![allow(dead_code)] #[derive(Debug)] enum Food { CordonBleu, Steak, Sushi } #[derive(Debug)] enum Day { Monday, Tuesday, Wednesday } // We dont have the ingredients to make Sushi. fn have_ingredients(food: Food) - OptionFood { match food { Food::Sushi None, _ Some(food), } } // We have the recipe for everything except Cordon Bleu. fn have_recipe(food: Food) - OptionFood { match food { Food::CordonBleu None, _ Some(food), } } // To make a dish, we need both the recipe and the ingredients. // We can represent the logic with a chain of matches: fn cookable_v1(food: Food) - OptionFood { match have_recipe(food) { None None, Some(food) have_ingredients(food), } } // This can conveniently be rewritten more compactly with and_then(): fn cookable_v3(food: Food) - OptionFood { have_recipe(food).and_then(have_ingredients) } // Otherwise wed need to flatten() an OptionOptionFood // to get an OptionFood: fn cookable_v2(food: Food) - OptionFood { have_recipe(food).map(have_ingredients).flatten() } fn eat(food: Food, day: Day) { match cookable_v3(food) { Some(food) println!(Yay! On {:?} we get to eat {:?}., day, food), None println!(Oh no. We dont get to eat on {:?}?, day), } } fn main() { let (cordon_bleu, steak, sushi) (Food::CordonBleu, Food::Steak, Food::Sushi); eat(cordon_bleu, Day::Monday); eat(steak, Day::Tuesday); eat(sushi, Day::Wednesday); }三种实现的语义完全相同——既要有食谱又要有食材才能做菜cookable_v1手写match先检查食谱食谱存在再去查食材cookable_v2mapflatten先得到OptionOptionFood再压平类型上可行但多一步cookable_v3and_then一步到位have_recipe(food)若为Some则把内层food直接喂给have_ingredients结果仍是OptionFood可以直接传给eat()。运行结果周一Cordon Bleu没有食谱、周三Sushi没有食材都打印Oh no...只有周二Steak打印Yay! On Tuesday we get to eat Steak.。默认值回退四件套or/or_else/get_or_insert/get_or_insert_with解包Option的另一个高频需求是取不到值就回退到默认值。Rust 提供了四个工具选择时只需回答两个问题要立即求值还是惰性求值要不要原地修改那个空值本身详见 defaults.mdor()可链式、急切求值、保持空值不动or()可链式调用且急切求值其参数——因此传入的变量会被移动move即使后续分支没有用到也已被消费#[derive(Debug)] enum Fruit { Apple, Orange, Banana, Kiwi, Lemon } fn main() { let apple Some(Fruit::Apple); let orange Some(Fruit::Orange); let no_fruit: OptionFruit None; let first_available_fruit no_fruit.or(orange).or(apple); println!(first_available_fruit: {:?}, first_available_fruit); // first_available_fruit: Some(Orange) // or moves its argument. // In the example above, or(orange) returned a Some, so or(apple) was not invoked. // But the variable named apple has been moved regardless, and cannot be used anymore. // println!(Variable apple was moved, so this line wont compile: {:?}, apple); // TODO: uncomment the line above to see the compiler error }注意其中的移动陷阱no_fruit.or(orange)已经得到Some(Orange)or(apple)的闭包逻辑虽然不会执行但apple作为参数已经按值传入并被移动此后无法再使用apple。取消注释最后一行即可让编译器报出所有权错误。or_else()可链式、惰性求值、保持空值不动or_else同样可链式但接受闭包并惰性求值——只有当前值为None时才会调用闭包#[derive(Debug)] enum Fruit { Apple, Orange, Banana, Kiwi, Lemon } fn main() { let no_fruit: OptionFruit None; let get_kiwi_as_fallback || { println!(Providing kiwi as fallback); Some(Fruit::Kiwi) }; let get_lemon_as_fallback || { println!(Providing lemon as fallback); Some(Fruit::Lemon) }; let first_available_fruit no_fruit .or_else(get_kiwi_as_fallback) .or_else(get_lemon_as_fallback); println!(first_available_fruit: {:?}, first_available_fruit); // Providing kiwi as fallback // first_available_fruit: Some(Kiwi) }输出显示no_fruit为None所以调用了get_kiwi_as_fallback得到Some(Kiwi)由于已获得Someget_lemon_as_fallback不会被调用——惰性求值在这里节省了一次闭包执行。get_or_insert()急切求值、原地修改空值与前两者保持空值不变不同get_or_insert会原地in place修改Option本身确保它持有值。它急切求值参数因此参数会被移动#[derive(Debug)] enum Fruit { Apple, Orange, Banana, Kiwi, Lemon } fn main() { let mut my_fruit: OptionFruit None; let apple Fruit::Apple; let first_available_fruit my_fruit.get_or_insert(apple); println!(first_available_fruit is: {:?}, first_available_fruit); println!(my_fruit is: {:?}, my_fruit); // first_available_fruit is: Apple // my_fruit is: Some(Apple) //println!(Variable named apple is moved: {:?}, apple); // TODO: uncomment the line above to see the compiler error }注意my_fruit声明为mut因为get_or_insert会改写它返回值first_available_fruit是对插入值的引用打印结果可见my_fruit已从None变为Some(Apple)。get_or_insert_with()惰性求值、原地修改空值get_or_insert_with接受闭包、惰性求值且若Option已有值则闭包不会执行、原值保持不变#[derive(Debug)] enum Fruit { Apple, Orange, Banana, Kiwi, Lemon } fn main() { let mut my_fruit: OptionFruit None; let get_lemon_as_fallback || { println!(Providing lemon as fallback); Fruit::Lemon }; let first_available_fruit my_fruit .get_or_insert_with(get_lemon_as_fallback); println!(first_available_fruit is: {:?}, first_available_fruit); println!(my_fruit is: {:?}, my_fruit); // Providing lemon as fallback // first_available_fruit is: Lemon // my_fruit is: Some(Lemon) // If the Option has a value, it is left unchanged, and the closure is not invoked let mut my_apple Some(Fruit::Apple); let should_be_apple my_apple.get_or_insert_with(get_lemon_as_fallback); println!(should_be_apple is: {:?}, should_be_apple); println!(my_apple is unchanged: {:?}, my_apple); // The output is a follows. Note that the closure get_lemon_as_fallback is not invoked // should_be_apple is: Apple // my_apple is unchanged: Some(Apple) }后半段的输出是重点my_apple已经是Some(Apple)此时get_or_insert_with直接返回现有值Providing lemon as fallback这条打印不会出现证明闭包确实未被调用。四个方法的速查方法求值方式是否原地修改典型场景or(value)急切移动参数否已有现成默认值可接受移动or_else(closure)惰性否默认值需要计算/可能昂贵get_or_insert(value)急切移动参数是需要确保Option持有值get_or_insert_with(closure)惰性是原地兜底且默认值惰性产生组合进Result体系transpose交换嵌套层Option常与Result混用。Rust By Example 在 PullingResults out ofOptions 中展示了混合错误类型的第一层解法——把两者互相嵌入。例如把向量首个字符串解析为整数并翻倍直接做法返回OptionResulti32, ParseIntErroruse std::num::ParseIntError; fn double_first(vec: Vecstr) - OptionResulti32, ParseIntError { vec.first().map(|first| { first.parse::i32().map(|n| 2 * n) }) }而transpose可以把OptionResult...与ResultOption...互换当你想遇到解析错误立即终止像?那样而遇到空向量却继续容忍时换成ResultOptioni32更顺手use std::num::ParseIntError; fn double_first(vec: Vecstr) - ResultOptioni32, ParseIntError { let opt vec.first().map(|first| { first.parse::i32().map(|n| 2 * n) }); opt.transpose() } fn main() { let numbers vec![42, 93, 18]; let empty vec![]; let strings vec![tofu, 93, 18]; println!(The first doubled is {:?}, double_first(numbers)); println!(The first doubled is {:?}, double_first(empty)); println!(The first doubled is {:?}, double_first(strings)); }三种输入对应三种结果Ok(Some(84))、Ok(None)空向量被容忍、Err(ParseIntError)tofu 解析失败被终止。这正是Option与Result在真实错误处理管线中协同作战的缩影。在 Rust By Example 中动手验证本文所有代码示例都来自仓库中的可编辑示例块。Rust By Example 的构建基于mdbook配置见 book.toml示例标注了editable可在页面上直接编辑运行与mdbook-runnable可执行等属性其中带ignore的示例如会panic的例子在页面上可运行但需要联网执行环境。本地复现方式来自 README.mdgit clone https://github.com/rust-lang/rust-by-example cd rust-by-example cargo install mdbook mdbook build mdbook servemdbook serve启动后即可在浏览器中打开对应章节Option unwrap一章位于 src/error/option_unwrap.md其四个子章节分别对应?运算符question_mark.md、mapmap.md、and_thenand_then.md与默认值回退defaults.md。整个错误处理专题的目录结构见 SUMMARY.md 的 Error handling 一节。小结从OptionT的两个变体出发Rust By Example 为我们铺开了一条完整的可选值处理路线match显式分支保证每条路径有处置unwrap/expect提供原型期与强约束场景下的快速取值?让深层解包不再嵌套map/and_then把变换与校验串成管道or家族四件套解决回退语义transpose则打通Option与Result的嵌套。其核心思想始终一致——把可能没有值写进类型里让编译器替你守住每一条缺失路径这正是 Rust 错误处理优雅且安全的关键所在。赞分享文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载相关推荐opencodex 用量面过滤器Usage Surface Filter实现解析区分 Codex 与 Claude 流量的服务端过滤、持久化与 GUI 设计opencodex 用量面过滤器Usage Surface Filter实现解析区分 Codex 与 Claude 流量的服务端过滤、持久化与 GUI 设文档教程100-exercises-to-learn-rust 第 07 关Result 的展开艺术——unwrap、expect 与 match 三选一100 exercises to learn rust 第 07 关 Result 的展开艺术—— unwrap 、 expect 与 match 三选一 导示例工程教程10分钟音频复刻目标音色RVC本地变声完整搭建指南10分钟音频复刻目标音色RVC本地变声完整搭建指南 10分钟干净人声就够在你自己的电脑上用RVCRetrieval based Voice Conver文档教程上一篇PptxGenJS用JavaScript代码实现企业级PPT自动化生成下一篇终极免费AI虚拟背景解决方案OBS Background Removal完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表