资讯动态

彻底搞懂Rust Result与Option:错误处理实战指南

发布时间:2026/10/14 4:13:21 来源:尧图企业网站定制
1. 为什么Result和Option让Rust程序员又爱又恨如果你写过几周Rust一定会对这两个类型有复杂的感情。一方面编译器强制你处理每一个可能失败的分支——文件可能不存在、整数可能溢出、配置可能缺失、网络可能超时Result和Option把这些不确定性都变成了类型系统的一部分让错误无处遁形。另一方面刚上手时那种怎么到处都是unwrap()的挫败感以及面对层层嵌套的match时想把键盘砸了的冲动我太理解了。但我要先说一个反直觉的结论Result和Option并不是为了折磨你而存在的。它们的存在本质上是为了把错误处理这件在几乎所有其他语言里都靠约定、文档、代码审查来约束的事情变成编译器能检查的强制规则。但这篇文章不打算讲那些教科书式的什么是Result什么是Option的基础概念——这些文档里都有。我想聊的是在实际项目中你是怎么从unwrap()满天飞过渡到一套清晰的错误处理策略的。包括错误类型怎么设计、?运算符怎么用才能不滥用、Option和Result怎么互相转换、以及那些你在文档里翻半天也未必找得到的实战细节。如果你已经能写出能编译的Rust代码但每次遇到复杂的错误处理场景还是要停下来想半天或者你的代码里充斥着unwrap()和expect()但你自己也知道这不是长久之计那么这篇文章就是写给你的。2. 从unwrap()到自定义错误类型先解决错误长什么样的问题很多人第一次写Rust项目时错误处理基本靠unwrap()和expect()。Demo阶段没问题但项目一跑起来用户输入了空字符串、配置文件少了一个字段、网络请求超时——程序直接panic屏幕上一堆Rust的panic信息用户一脸懵。这时候你自然想到我需要一个自定义错误类型。但怎么设计这个类型其实大有讲究。2.1 枚举错误vs字符串错误别急着用Boxdyn Error我见过很多初学者一上来就写Boxdyn Error觉得省事。确实Boxdyn Error能让你把任何实现了std::error::Error的错误塞进去配合?运算符非常爽。但它的缺点也很明显调用方拿到的是一个不透明的错误难以针对不同错误类型做不同的处理逻辑。更推荐的做法是为你的模块或项目定义一个错误枚举。比如你在写一个配置加载模块use std::fmt; #[derive(Debug)] pub enum ConfigError { Io(std::io::Error), Parse(String), MissingField(static str), } impl fmt::Display for ConfigError { fn fmt(self, f: mut fmt::Formatter_) - fmt::Result { match self { ConfigError::Io(e) write!(f, IO错误: {}, e), ConfigError::Parse(s) write!(f, 解析失败: {}, s), ConfigError::MissingField(name) write!(f, 缺少必要字段: {}, name), } } } impl std::error::Error for ConfigError { fn source(self) - Option(dyn std::error::Error static) { match self { ConfigError::Io(e) Some(e), _ None, } } }我当年第一次看到这套写法时内心是崩溃的为什么我要手写这么多代码就为了一个错误类型但后来我想明白了一件事错误类型是你的模块对外API的一部分。就像函数的参数和返回值类型一样错误类型决定了调用方能怎么处理失败。如果你的错误类型是个黑盒调用方就只能把错误继续往上抛或者随便打印一下。但如果错误类型是枚举调用方就可以针对不同类型的错误做出不同的响应。2.2 手动实现From让?运算符真正好用光定义错误类型还不够。想想看你写let file File::open(path)?;的时候?会把std::io::Error自动转换成你的ConfigError。为什么因为有From实现。impl Fromstd::io::Error for ConfigError { fn from(e: std::io::Error) - Self { ConfigError::Io(e) } }类似地如果你的解析逻辑用到了serde_json::Error那就再加一个impl Fromserde_json::Error for ConfigError { fn from(e: serde_json::Error) - Self { ConfigError::Parse(e.to_string()) } }每当你用?引入一个新的错误来源就需要补充对应的From实现。这看起来是重复劳动但它有一个隐蔽的好处你的错误边界是明确的。如果某个函数返回Result_, ConfigError调用方就知道这个函数只会产生你声明的那几种错误不会有意想不到的幺蛾子。我个人的建议是小型项目或者临时脚本直接用Boxdyn Error快速搞定没问题但凡是会被人当作库来用、或者要长期维护的项目一开始就花半小时把错误枚举建好后面会省下无数排查问题的时间。3. 组合子与?运算符从逐层match到做减法等你有了自定义错误类型下一步就是处理那些连续调用、每一步都可能失败的代码。最朴素的做法是嵌套match——但写过一次的人都不想写第二次。看一个典型场景读取一个文件解析里面的JSON再从JSON里取一个字段然后解析成整数。3.1 传统match写法和它的痛点fn parse_count(path: str) - Resulti32, ConfigError { let content match fs::read_to_string(path) { Ok(c) c, Err(e) return Err(ConfigError::Io(e)), }; let v: serde_json::Value match serde_json::from_str(content) { Ok(v) v, Err(e) return Err(ConfigError::Parse(e.to_string())), }; let count match v.get(count) { Some(c) c, None return Err(ConfigError::MissingField(count)), }; match count.as_i64() { Some(n) Ok(n as i32), None Err(ConfigError::Parse(count不是数字.to_string())), } }这段代码的真实逻辑其实只有四行读文件、解析JSON、取字段、转数字。但为了处理错误你写了两倍的代码。而且这种写法一旦嵌套超过两层可读性就会急剧下降。3.2 用?运算符精简同样的逻辑用?运算符可以这样写fn parse_count(path: str) - Resulti32, ConfigError { let content fs::read_to_string(path)?; let v: serde_json::Value serde_json::from_str(content)?; let count v.get(count).ok_or(ConfigError::MissingField(count))?; count.as_i64() .map(|n| n as i32) .ok_or(ConfigError::Parse(count不是数字.to_string())) }从8个match缩减到2次?和2次ok_or逻辑一眼就能扫完。这里的核心变化是?负责处理错误类型能自动转换的情况把你的From实现用起来了。Option类型本身没有?的错误类型但可以通过ok_or或ok_or_else转成Result然后继续用?。3.3 ok_or vs ok_or_else懒求值才是正确选择这里有个小细节值得展开ok_or的参数是急求值的ok_or_else的参数是闭包、惰性求值。// 每次都会先构造错误 let x opt.ok_or(ConfigError::MissingField(count))?; // 只有opt是None时才构造错误 let x opt.ok_or_else(|| ConfigError::MissingField(count))?;如果错误类型的构造逻辑很轻量比如就是包一个枚举变体两者差别不大。但如果错误构造涉及堆分配、格式化字符串、日志输出ok_or就会在成功路径上也白白付出成本。我的习惯是一律用ok_or_else养成一个好习惯比每次都纠结强。另外还有一个常见的组合子链我得提醒一下新手map_err不要滥用。有时候你看到别人的代码是?接?再接map_err觉得啊好优雅。但如果你发现自己在每条错误路径上都要map_err来做字符串拼接那大概率是错误类型设计得不对——应该让From实现搞定这些转换而不是在外面层层包裹。4. Option不是Result的穷亲戚它与Result的分工与合作很多人会下意识觉得Option就是Result的一个简化版——无非是去掉了错误信息而已。但这个看法太浅了。在实际项目里Option和Result解决的是两类本质不同的问题ResultT, E这次操作预期可能失败失败时你要知道失败原因。OptionT这个值可能存在也可能根本不在不存在本身是正常的不是错误。一个典型例子从HashMap里查一个键。键不存在是正常的——你可能想先查缓存再查数据库查不到就返回默认值。这种情况下用Option表达没查到语义更准确用Result反而别扭难道要返回Err(not found)吗那调用方还得把没有和出错区分开徒增负担。4.1 不要一看到Option就想着unwrap我见过不少Rust代码里面充斥着这样的模式if let Some(v) map.get(key) { // 用v } else { // fallback逻辑 }这本身没问题。但还有更大的空间。Option有一整套组合子很多场景下比if let更短且更不容易出错// 取不到就用默认值 let v map.get(key).copied().unwrap_or(0); // 取不到就计算一个默认值惰性 let v map.get(key).cloned().unwrap_or_else(|| expensive_default()); // 链式查找缓存里没有就查配置 let v cache.get(key) .or(config.get(key)) .or(Some(global_default)) .copied() .unwrap_or(0);看到.or()这个吗它很妙左侧是Option右侧是Option如果左侧是None就返回右侧。这一条链就把多级回退写成了声明式的代码比一堆if let嵌套干净太多。4.2 从Result到Option的转换别写if x.is_err()在业务代码里你经常会遇到不在乎失败原因只想知道有没有成功的情况。比如向一个指标收集系统上报数据失败了也无所谓反正下次还会继续报。这时候把Result转成Option就很有用// 笨办法 let maybe_value if result.is_ok() { Some(result.unwrap()) } else { None }; // 规范做法 let maybe_value result.ok(); // 如果要保留错误只是用不上可以打日志 let maybe_value result.inspect_err(|e| log::warn!(上报失败: {}, e)).ok();Result::ok()和Result::err()这两个转换方法简单直接。inspect_err是Rust 1.76版本加入的方法能在不消费Result的情况下对错误做副作用操作比如打日志之后再继续链式调用。这个API出现之前大家要么用map_err(|e| { log::warn!(...); e })要么先if let Err(e)再继续。现在舒服多了。4.3 另外一个方向把Option变成Result反过来从Option到Result的转换也很常见特别是当你写的函数返回Result但内部有一个返回Option的调用fn get_user_display_name(user_id: UserId) - ResultString, UserError { let name db.find_name(user_id) .ok_or(UserError::NotFound(*user_id))?; // 继续处理... Ok(name) }这里ok_or的作用是给None补上一个具体的错误信息。注意错误信息里带上了user_id这个习惯很重要——调试线上问题的时候一个包含上下文信息的错误比一个干巴巴的NotFound有用十倍。如果你用的是anyhow可能还会再.context()补一层。最后聊一个我踩过的坑不要用transpose()来反转Option和Result的关系除非你确实知道自己在干什么。transpose()能把OptionResultT, E变成ResultOptionT, E这个操作在某些场景下很优雅但它隐含的语义是外部Option优先于内部Result。如果你只是想在Result链里插入一个Option判断用ok_or更直接、更不容易出乱子。5. unwrap()和expect()的安全使用范围——实战中的分寸感我知道只要讨论错误处理就一定会有人说不要用unwrap()。但我更想说unwrap()本身不是罪滥用才是。一个经验丰富的Rust开发者不是那种永远不使用unwrap()的人而是清楚地知道哪些unwrap()是绝对安全的、哪些是需要用expect()留下线索的、哪些是应该用错误传播替代的。5.1 绝对安全的unwrap有一些场景你可以100%确信unwrap不会panic而且编译器也没法证明这一点——这时候用unwrap()是可接受的let numbers 1,2,3; let parsed: Veci32 numbers.split(,).map(|s| s.parse().unwrap()).collect();这行代码的unwrap可能panic吗如果字符串内容是你自己写的或已经被验证过的那么不会。但问题是如果以后有人把字符串来源改成了用户输入这个unwrap就变成了一颗地雷。所以更好的习惯是在这种我知道它不会失败但编译器不知道的场景用expect来留下说明let parsed: Veci32 numbers.split(,).map(|s| s.parse().expect(逗号分隔的字符串中每一段都必须是数字)).collect();这样将来如果有人把这段代码改坏了panic信息里会直接告诉你为什么这个unwrap是安全的这个前提被破坏了而不是显示一个莫名其妙的ParseIntError。5.2 真正要避免的是不确定的unwrap真正危险的是那些你说不出为什么不会失败的unwrap。举个例子let first args.get(1).unwrap(); // 为什么一定存在除非你能确保调用方一定传入了至少一个参数否则这个unwrap就是在赌。正确的做法是什么是让这个信息流动起来let Some(first) args.get(1) else { println!(用法: mytool input); std::process::exit(1); };Rust 1.65之后引入了let-else语法处理这种需要提前回归的场景非常顺手。它比if let ... else { return; }更紧凑而且编译器会强制你处理else分支。遇到这种没有合理的默认值、失败就应该退出的情况我强烈推荐用它。5.3 大型项目中的unwrap策略如果你在维护一个中大型Rust项目光是约定不用unwrap是不够的。我建议在项目的Cargo.toml里配置clippy的lint规则把可能panic的unwrap列为警告[lints.clippy] unwrap_used warn expect_used warn这样每次cargo clippy都会提醒你这里有个unwrap逼迫你去思考它是否绝对安全。如果确实是绝对安全的而且你想让它通过检查就显式地#[allow(clippy::unwrap_used)]并附上注释说明原因。我自己在项目里就是这么干的。刚开始会觉得自己在跟编译器较劲但坚持一两个月之后你会发现自己写新代码时已经不太会自然地打出unwrap了——因为脑子已经形成了找错误传播路径的肌肉记忆。5.4 关于panic的另一个态度panic也是一种错误处理策略最后想聊一个很多人讳莫如深的话题panic不全是坏事。在Rust里panic表示不可恢复的错误——程序状态已经不合法了继续跑下去会产生更严重的问题。这种情况你还硬撑着返回一个Result反而是错误的。最典型的例子是let even |n: u32| n % 2 0; let _ even(2); // 正常但如果你在写一个只在数学意义上合法的函数入参违反前置条件时panic是合理的fn divide(a: f64, b: f64) - f64 { assert!(b ! 0.0, 除数为0); a / b }这个assert!就是在声明程序出错也不要怪我。把这种前置条件用Result来表达反而会让调用方的错误处理逻辑复杂化——毕竟几乎没有哪个业务代码能优雅地处理除数为0。所以能用Result表达的业务失败用Result前置条件被破坏、程序已经不可能正确运行的情况用panic是合理的。重要的是你要分清楚自己在哪种情况下。6. 真实项目中的错误处理模式一个完整的流程示例前面讲了很多零散的技巧但你可能还是想知道在一个真实项目里这些东西是怎么串起来的我来演示一个比较典型的场景写一个命令行工具读取配置文件校验配置连接数据库执行SQL返回结果。这个场景涵盖了从错误定义、错误转换、到Option处理的一系列问题。6.1 定义错误体系首先定义整个项目统一的错误类型。一个常见做法是区分底层错误和业务错误但小项目没必要搞两套一个枚举就够了。#[derive(Debug)] pub enum AppError { Config(ConfigError), Db(sqlx::Error), NotFound(String), InvalidInput(String), } impl fmt::Display for AppError { /* ... */ } impl std::error::Error for AppError { /* ... */ } impl FromConfigError for AppError { fn from(e: ConfigError) - Self { AppError::Config(e) } } impl Fromsqlx::Error for AppError { fn from(e: sqlx::Error) - Self { AppError::Db(e) } }注意我使用了sqlx::Error作为数据库错误类型的代表。实际项目中你的错误可能来自HTTP请求、Redis、消息队列、文件系统——每接入一个第三方库就补一个From实现。这确实是体力活但它保证了你App所有错误都能在AppError里找到对应的变体不会出现错误类型是别的什么东西的情况。6.2 写出以?为主线的业务函数然后是你的核心业务逻辑全部用?串起来pub async fn run_query(user_input: str) - ResultQueryResult, AppError { // 配置加载 let config Config::load(app.toml)?; // 参数校验 if user_input.trim().is_empty() { return Err(AppError::InvalidInput(查询内容不能为空.to_string())); } // 连接数据库连接池是在配置里预建的这里只是获取连接 let mut conn config.pool.acquire().await?; // 执行查询 let result sqlx::query_as::_, QueryResult(SELECT ...) .bind(user_input) .fetch_optional(mut *conn) .await? .ok_or(AppError::NotFound(没有找到匹配的数据.into()))?; Ok(result) }注意几处关键设计Config::load(app.toml)?返回ResultConfig, ConfigError因为AppError实现了FromConfigError所以?自动转换。pool.acquire().await?返回ResultConnection, sqlx::Error同样自动转换。fetch_optional返回的是OptionQueryResult用.ok_or(AppError::NotFound(...))?把没数据这一正常情况映射成业务错误。整个函数的逻辑非常线性从前往后每一步都可能失败每步的失败直接?抛上去。最终在边界处统一处理。6.3 边界处统一处理错误边界处是什么是main函数或者某条HTTP路由的handler。在这里你不会再让AppError继续往外抛而是把它转成用户能看到的东西#[tokio::main] async fn main() { let args: VecString std::env::args().collect(); match run_query(args.get(1).map(|s| s.as_str()).unwrap_or()).await { Ok(result) println!(查询结果: {}, result), Err(e) { eprintln!(出错了: {}, e); // 可以按不同类型给出不同提示 match e { AppError::NotFound(_) eprintln!(请检查查询条件是否有误), AppError::InvalidInput(_) eprintln!(请检查输入参数格式), _ eprintln!(系统内部错误请稍后重试), } std::process::exit(1); } } }看到那个unwrap_or()了吗这就是绝对安全unwrap的替代品——命令行参数可能没有第二个但空字符串传给run_query会被InvalidInput分支挡住所以这里用unwrap_or给一个空默认值就很好地处理了边界情况。这个边界统一处理的设计思路其实和Web框架里全局错误处理器是一样的逻辑。你在内层函数里用?传播错误让控制流保持线性在外层一次性捕获所有错误翻译成用户能理解的信息。如果你把错误处理散落在每一个中间层代码很快就会乱成一锅粥。6.4 处理第三方库错误时的上下文问题前面我实现了Fromsqlx::Error for AppError但这样转换过来的错误信息可能不够。比如sqlx的连接超时错误Display可能只告诉你connection timed out但你不一定记得是连哪个数据库超时的。这时候有两个选择在错误枚举变体里带上上下文信息比如AppError::Db { source: sqlx::Error, context: String }。代价是错误类型复杂一些。用map_err在转换时补充上下文let pool sqlx::PgPool::connect(config.database_url) .await .map_err(|e| AppError::Db(e)) .map_err(|e| AppError::Config(ConfigError::Other(format!(数据库连接失败 (URL: {}): {}, config.database_url, e))))?;这种方式比较啰嗦但在关键节点比如连接数据库这类一站错就全盘皆输的地方补一层上下文排查问题的速度会快一个数量级。如果项目用到anyhow可以用它的Contexttrait更优雅地做这件事。但那是另一种错误处理哲学用anyhow把错误都变成字符串上下文和自定义枚举错误互有取舍。我个人的选择是库内部、需要精确控制错误变体的代码用自定义枚举应用层、快速的CLI工具或Service代码用anyhow快速迭代。两种思路掺着用也可以但要在项目内部约定清楚分界线。7. 进阶技巧OptionResult 、ResultOption 和flatten()家族等上面这些基础都熟练了你会在代码里碰到一些复合形状的类型。最常见的两个是OptionResultT, E和ResultOptionT, E。它们看起来很像但语义完全不同处理方式也不同我单独拿一节来说。7.1 OptionResultT, E先有值再处理值中的失败这种类型常见于迭代器。比如你有一个VecString想把每个字符串parse成整数let items vec![1.to_string(), 2.to_string(), abc.to_string()]; let parsed: VecResulti32, std::num::ParseIntError items.iter().map(|s| s.parse::i32()).collect();这里每个元素都是一个Result。你可以用collect()直接收集成ResultVeci32, _这样只要有一个解析失败整个收集就会失败let all_parsed: ResultVeci32, _ items.iter().map(|s| s.parse::i32()).collect(); // Ok([1, 2]) 或者 Err(ParseIntError)如果你想要跳过失败的元素而不是整体失败呢那可以用filter_maplet valid_only: Veci32 items .iter() .filter_map(|s| s.parse::i32().ok()) .collect(); // [1, 2] — abc被跳过了7.2 ResultOption , E失败要报告但没结果是正常的这种类型常见于查询数据库类操作fetch_optional返回的就是这个形状。它表达三层意思查询操作本身可能出错Err。操作成功但可能没有匹配的记录Ok(None)。操作成功并且有结果Ok(Some(value))。处理这种类型时最容易犯的错是把Ok(None)当成错误。很多新手会写let row query(...).await?; // ResultOptionRow, _ let row row.ok_or(not found)?; // 强行把Ok(None)转成错误但没有数据在某些场景下可能不是错误——比如要求如果用户不存在就返回默认的角色权限。这时候你应该let row query(...).await?; // ResultOptionRow, _ let permissions match row { Some(r) r.get_permissions(), None default_permissions(), };如果确实想让没数据变成错误那ok_or没问题但请注意区分业务上没数据和查询失败是两种不同的情况它们的处理方式、错误提示、日志级别都应该不一样。混为一谈的话上下游交接时很容易产生歧义。7.3 flatten()把嵌套打平Rust 1.60之后Option::flatten和Result::flatten稳定了。它们解决的是嵌套的相同Type问题// OptionOptionT - OptionT let nested: OptionOptioni32 Some(Some(5)); let flat: Optioni32 nested.flatten(); // Some(5) // ResultResultT, E, E - ResultT, E let nested: ResultResulti32, str, str Ok(Ok(5)); let flat: Resulti32, str nested.flatten(); // Ok(5)这个API看起来平淡无奇但配合Iterator::map可以减少不少嵌套。比如你有一个HashMapString, Optioni32想安全地取出某个key的值再进一步做链式操作let data: HashMapString, Optioni32 ...; let result data.get(key) // OptionOptioni32 .copied() // OptionOptioni32 .flatten() // Optioni32 .map(|n| n * 2) .unwrap_or(0);如果你不用flatten这段代码就得写成let result match data.get(key) { Some(Some(n)) n * 2, _ 0, };两种写法都能看懂但前者的链式风格在一条表达式里完成多级Optional取值时更顺手尤其适合函数式风格的代码。7.4 组合拳示例一个我经常用的组合是迭代器 filter_map Result 提前返回。比如你想要从一堆配置项里解析所有数字如果任何一个不合规就返回具体的错误名fn parse_all(keys: [String], config: HashMapString, String) - ResultVeci32, String { keys.iter().map(|k| { config.get(k) .ok_or_else(|| format!(缺少配置项: {}, k)) .and_then(|v| v.parse::i32().map_err(|_| format!(配置项 {} 不是数字: {}, k, v))) }).collect() }这里展示的是Result的and_then用法前一个步骤的结果Ok分支的String作为后一个步骤的输入。ok_or_else负责配置缺失的错误and_then内部的map_err负责格式错误的错误。最终用collect()做短路——只要有一个失败整个ResultVeci32, String就是Err。这种把多个容易出错的步骤组合成一个表达式的写法是Rust错误处理里最优雅的部分。你不需要写一堆局部变量和if let所有分支都通过组合子串起来。前提是对这些组合子的行为有足够清晰的认知——所以我还是建议先花时间把Option和Result的所有方法都过一遍map、and_then、or_else、ok_or、flatten、transpose不需要背但要能一眼看出某个场景适合用哪个。8. 避免过度设计错误处理也要讲性价比这篇博文到这里已经写得比较全了但我想最后泼一盆冷水错误处理不是越复杂越好过度设计同样是一个陷阱。有不少人从unwrap滥用的极端走向了为每个错误建立三层抽象的另一个极端。比如在枚举错误里面再嵌套枚举错误。每个函数都写一个独立的错误类型。所有错误都加上backtrace、UID、时间戳、本地化文案……然后发现整个项目一半的代码都在处理错误。这些做法让代码变得极其难读。更严重的后果是团队里其他人看到你的错误类型设计得这么重内心会对增加新的错误分支产生抵触于是有些不该被忽略的问题也被塞进一个笼统的变体里。我对错误处理的定级标准是这样的暴露面级别推荐方案内部私有函数、闭包OptionT或ResultT, String够用不必建枚举模块内的公开函数模块级错误枚举字段信息尽量精简crate对外公开API精心设计的错误枚举实现std::error::Error提供source()应用main函数anyhow::Result或统一错误类型最终打印/记录我这几年写Rust的体会是错误类型的设计是一个逐步演进的过程。起步阶段别想着一口气设计完美先用简单的方案跑通逻辑等项目的边界清晰了、调用关系稳定了再回头把错误处理收敛成统一模式。Rust的编译器会帮我们兜底——所有没处理的分支都会在编译期暴露出来晚一点重构也没问题反正编译器会提醒你哪里漏了。另外提一句经常被忽视的事写测试的时候别忘了测错误路径。#[should_panic]可以测panic的情况assert!(result.is_err())可以测错误返回。尤其要测那些本来以为不会失败的unwrap会不会真的panic——很多线上事故其实就是作者以为这个分支永远不会走结果被用户一通操作触发了。测试是你最后一道防线。9. 写在最后错误处理是一种需要长期打磨的手感Rust的错误处理表面上是API使用问题本质上是工程判断力问题。同一个操作用unwrap、用expect、用ok_or、用自定义错误枚举往往都能通过编译但代码的健壮性、可维护性、排障效率天差地别。这种手感不是看看文档就能建立的一定是在真实的项目中、在踩过几个坑之后慢慢沉淀出来的。我个人最推荐的练习方式是把你之前写过的一个小工具用Rust重写一遍强制自己不用unwrap所有错误都用?和自定义错误类型处理。做完这个练习之后你会对Result和Option的配合有完全不同的理解。如果你的项目还处在起步阶段不妨就从自定义错误枚举 ?运算符 ok_or_else这三板斧开始。它们足够覆盖大部分日常场景又不会让你陷入过度设计。等真的碰到错误信息不够用需要区分错误来源的时候再逐步补充上下文和错误链。记住那句我常对自己说的话先能编译再谈优雅先能运行再谈健壮。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑