首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >12-Rust 教程 - 错误处理

12-Rust 教程 - 错误处理

作者头像
LarryLan
发布2026-04-28 12:24:01
发布2026-04-28 12:24:01
1070
举报

错误处理

panic!、Result、? 运算符:让错误不再是程序崩溃的理由

🎬 引入

你有没有经历过这种绝望:程序跑了一半,突然崩溃,屏幕上只留下一行冰冷的错误信息,而你根本不知道发生了什么?

在 Rust 之前,我写 Python 的时候,try-except 就像个万能创可贴,哪里出错贴哪里。到了 Rust 才发现,人家把错误处理玩成了艺术——要么你优雅地处理错误,要么程序就明明白白地告诉你"我不干了"。

Rust 有两种错误处理方式:panic!(直接摆烂)和 Result(优雅处理)。今天咱们就搞清楚什么时候该摆烂,什么时候该优雅。

📌 核心概念

错误分类

Rust 把错误分成两类:

1. 可恢复错误 - 比如文件不存在、网络超时、数据格式错误。这些错误可以处理,程序还能继续跑。

2. 不可恢复错误 - 比如数组越界、除零、逻辑 bug。这些错误说明程序状态已经不对了,继续跑只会更糟。

panic! - 直接摆烂

panic! 就是程序在说:"我遇到无法处理的错误了,不玩了!"然后立即终止。

什么时候用 panic!:

  • 原型开发阶段(先跑起来再说)
  • 测试代码
  • 确实无法恢复的错误(比如配置加载失败)
  • 你确定这个错误不应该发生(bug)

Result<T, E> - 优雅处理

Result 是个枚举,只有两种状态:

  • Ok(T) - 成功了,返回值是 T
  • Err(E) - 失败了,错误信息是 E

这就像你去餐厅点菜:

  • Ok(你的菜) - 菜做好了
  • Err(厨师的抱怨) - 菜没了/做糊了/厨师罢工了

? 运算符 - 错误传递神器

? 运算符的作用是:"如果成功了继续,如果失败了直接把错误返回给调用者"。

这就像你老板让你办事:

  • 事情办成了 → 继续下一步
  • 事情办砸了 → 直接告诉老板"办不了",让老板自己决定

💻 代码示例

panic! 示例

代码语言:javascript
复制
fn main() {
    // 显式 panic
    panic!("我不干了!");
    
    // 常见 panic 场景
    let nums = vec![, , ];
    let num = nums[];  // ❌ panic! 索引越界
    
    let x = ;
    let y =  / x;  // ❌ panic! 除零(debug 模式)
}

Result 基础

代码语言:javascript
复制
use std::fs::File;
use std::io::Error;

fn open_file() -> Result<File, Error> {
    // File::open 返回 Result<File, Error>
    let file = File::open("hello.txt");
    
    match file {
        Ok(f) => {
            println!("文件打开成功!");
            Ok(f)
        }
        Err(e) => {
            println!("文件打开失败:{}", e);
            Err(e)
        }
    }
}

fn main() {
    match open_file() {
        Ok(_) => println!("搞定!"),
        Err(_) => println!("完蛋!"),
    }
}

? 运算符

代码语言:javascript
复制
use std::fs::File;
use std::io::{self, Read};

// 用 ? 简化错误处理
fn read_file() -> io::Result<String> {
    let mut file = File::open("hello.txt")?;  // 失败直接返回 Err
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;      // 失败直接返回 Err
    Ok(contents)
}

// 链式调用 + ?
fn read_file_short() -> io::Result<String> {
    let mut contents = String::new();
    File::open("hello.txt")?
        .read_to_string(&mut contents)?;
    Ok(contents)
}

自定义错误类型

代码语言:javascript
复制
use std::fmt;
use std::error::Error;

// 定义自己的错误类型
#[derive(Debug)]
enum AppError {
    FileNotFound(String),
    InvalidData(String),
    DatabaseError(String),
}

// 实现 Display trait
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::FileNotFound(path) => write!(f, "文件不存在:{}", path),
            AppError::InvalidData(msg) => write!(f, "数据无效:{}", msg),
            AppError::DatabaseError(msg) => write!(f, "数据库错误:{}", msg),
        }
    }
}

// 实现 Error trait
impl Error for AppError {}

// 使用自定义错误
fn load_config(path: &str) -> Result<String, AppError> {
    if path.is_empty() {
        return Err(AppError::FileNotFound("路径为空".to_string()));
    }
    
    // 模拟读取文件
    Ok("配置内容".to_string())
}

fn main() {
    match load_config("") {
        Ok(config) => println!("配置:{}", config),
        Err(e) => println!("错误:{}", e),
    }
}

错误转换

代码语言:javascript
复制
use std::fs::File;
use std::io;
use std::num::ParseIntError;

// 多种错误类型统一处理
fn read_number() -> Result<i32, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string("number.txt")?;  // io::Error
    let number: i32 = contents.trim().parse()?;              // ParseIntError
    Ok(number)
}

// 用 map_err 转换错误
fn parse_positive() -> Result<u32, String> {
    let s = "42";
    s.parse::<u32>()
        .map_err(|e| format!("解析失败:{}", e))
        .and_then(|n| {
            if n >  {
                Ok(n)
            } else {
                Err("数字必须大于 0".to_string())
            }
        })
}

🐛 错误示例 - 这些坑你别踩

错误 1:忽略 Result

代码语言:javascript
复制
fn main() {
    File::open("hello.txt");  // ⚠️ 警告!Result 被忽略
}

编译器说:

代码语言:javascript
复制
warning: unused `Result` that must be used
note: this may be a bug and should be handled

正确做法:

代码语言:javascript
复制
let _ = File::open("hello.txt");  // 显式忽略
// 或者
File::open("hello.txt").unwrap();  // panic 如果失败
// 或者
match File::open("hello.txt") {
    Ok(f) => {/* 处理 */},
    Err(e) => {/* 处理错误 */}
}

错误 2:滥用 unwrap()

代码语言:javascript
复制
fn main() {
    let file = File::open("hello.txt").unwrap();  // ❌ 文件不存在就 panic
}

正确做法:

代码语言:javascript
复制
let file = match File::open("hello.txt") {
    Ok(f) => f,
    Err(e) => {
        eprintln!("无法打开文件:{}", e);
        return;
    }
};

错误 3:? 用在 main 函数外

代码语言:javascript
复制
fn read_file() {
    let contents = std::fs::read_to_string("hello.txt")?;  // ❌ 编译错误
}

编译器说:

代码语言:javascript
复制
the `?` operator can only be used in a function that returns `Result` or `Option`

正确做法:

代码语言:javascript
复制
fn read_file() -> std::io::Result<()> {
    let contents = std::fs::read_to_string("hello.txt")?;
    Ok(())
}

🐛 常见坑点

1. 错误类型不匹配

代码语言:javascript
复制
fn process() -> Result<(), String> {
    let file = File::open("hello.txt")?;  // ❌ io::Error 不能转 String
    Ok(())
}

解决:

代码语言:javascript
复制
fn process() -> Result<(), Box<dyn Error>> {
    let file = File::open("hello.txt")?;  // ✅ 统一用 Box<dyn Error>
    Ok(())
}

2. 过度使用 ?

代码语言:javascript
复制
fn validate(x: i32) -> Result<i32, String> {
    if x <  {
        return Err("负数".to_string());
    }
    Ok(x * )
}

fn process() -> Result<(), String> {
    let x = ;
    let y = validate(x)?;  // ✅ 可以
    let z = validate(y)?;  // ✅ 可以
    // ... 一连串 ?
    Ok(())
}

如果中间需要特殊处理某个错误,就别用 ?,用 match

3. panic! 在生产环境

代码语言:javascript
复制
fn load_config() -> Config {
    let config = Config::load();
    config.unwrap()  // ❌ 生产环境别这么干
}

正确做法: 返回 Result,让调用者决定怎么处理。

🎯 实战案例

案例 1:配置文件加载器

代码语言:javascript
复制
use std::fs;
use std::io;
use std::path::Path;

#[derive(Debug)]
enum ConfigError {
    Io(io::Error),
    Parse(String),
    MissingField(String),
}

impl From<io::Error> for ConfigError {
    fn from(err: io::Error) -> Self {
        ConfigError::Io(err)
    }
}

struct Config {
    host: String,
    port: u16,
}

impl Config {
    fn load(path: &str) -> Result<Self, ConfigError> {
        let contents = fs::read_to_string(path)?;
        
        let mut host = None;
        let mut port = None;
        
        for line in contents.lines() {
            let parts: Vec<&str> = line.split('=').collect();
            if parts.len() !=  {
                continue;
            }
            
            match parts[].trim() {
                "host" => host = Some(parts[].trim().to_string()),
                "port" => port = Some(parts[].trim().parse()
                    .map_err(|e| ConfigError::Parse(format!("端口解析失败:{}", e)))?),
                _ => {}
            }
        }
        
        Ok(Config {
            host: host.ok_or_else(|| ConfigError::MissingField("host".to_string()))?,
            port: port.ok_or_else(|| ConfigError::MissingField("port".to_string()))?,
        })
    }
}

fn main() {
    match Config::load("config.txt") {
        Ok(config) => println!("配置加载成功:{}:{}", config.host, config.port),
        Err(e) => eprintln!("配置加载失败:{:?}", e),
    }
}

案例 2:用户输入验证

代码语言:javascript
复制
#[derive(Debug)]
enum ValidationError {
    TooShort,
    TooLong,
    InvalidCharacter(char),
    Empty,
}

impl std::fmt::Display for ValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ValidationError::TooShort => write!(f, "用户名太短"),
            ValidationError::TooLong => write!(f, "用户名太长"),
            ValidationError::InvalidCharacter(c) => write!(f, "包含非法字符:{}", c),
            ValidationError::Empty => write!(f, "用户名不能为空"),
        }
    }
}

fn validate_username(username: &str) -> Result<String, ValidationError> {
    if username.is_empty() {
        return Err(ValidationError::Empty);
    }
    
    if username.len() <  {
        return Err(ValidationError::TooShort);
    }
    
    if username.len() >  {
        return Err(ValidationError::TooLong);
    }
    
    for c in username.chars() {
        if !c.is_alphanumeric() && c != '_' {
            return Err(ValidationError::InvalidCharacter(c));
        }
    }
    
    Ok(username.to_string())
}

fn main() {
    let usernames = vec!["", "ab", "valid_user", "very_long_username_that_is_too_long", "bad@user"];
    
    for username in usernames {
        match validate_username(username) {
            Ok(name) => println!("✅ {} 验证通过", name),
            Err(e) => println!("❌ {} 验证失败:{}", username, e),
        }
    }
}

案例 3:批量文件处理器

代码语言:javascript
复制
use std::fs;
use std::io;
use std::path::PathBuf;

struct ProcessResult {
    success: usize,
    failed: usize,
    errors: Vec<(String, String)>,
}

fn process_files(paths: &[String]) -> ProcessResult {
    let mut result = ProcessResult {
        success: ,
        failed: ,
        errors: Vec::new(),
    };
    
    for path in paths {
        match process_single_file(path) {
            Ok(_) => result.success += ,
            Err(e) => {
                result.failed += ;
                result.errors.push((path.clone(), e.to_string()));
            }
        }
    }
    
    result
}

fn process_single_file(path: &str) -> io::Result<()> {
    let contents = fs::read_to_string(path)?;
    let processed = contents.to_uppercase();
    fs::write(path, processed)?;
    Ok(())
}

fn main() {
    let files = vec![
        "file1.txt".to_string(),
        "file2.txt".to_string(),
        "nonexistent.txt".to_string(),
    ];
    
    let result = process_files(&files);
    
    println!("处理完成:成功 {}, 失败 {}", result.success, result.failed);
    
    for (path, error) in result.errors {
        eprintln!("文件 {} 处理失败:{}", path, error);
    }
}

🧠 思维导图

12-错误处理
12-错误处理

📝 小结

  1. 两种错误:可恢复的用 Result,不可恢复的用 panic!
  2. Result 是枚举Ok(T) 成功,Err(E) 失败,必须处理
  3. ? 运算符:错误传递神器,但函数必须返回 ResultOption
  4. 自定义错误:用枚举定义多种错误,实现 DisplayError trait
  5. 别滥用 unwrap():生产环境要优雅处理错误,别动不动就 panic

下篇预告: 你有没有写过重复的代码?同样的逻辑,不同的类型,就要复制粘贴?泛型来拯救你了!一篇让你搞清楚泛型到底是个啥,为什么 Rust 的泛型这么强大!

🔗 参考资料

  • Rust Book - Error Handling
  • std::result::Result
  • std::panic
  • Rust By Example - Error
  • thiserror crate - 简化错误定义
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 错误处理
    • 🎬 引入
    • 📌 核心概念
      • 错误分类
      • panic! - 直接摆烂
      • Result<T, E> - 优雅处理
      • ? 运算符 - 错误传递神器
    • 💻 代码示例
      • panic! 示例
      • Result 基础
      • ? 运算符
      • 自定义错误类型
      • 错误转换
      • 🐛 错误示例 - 这些坑你别踩
    • 🐛 常见坑点
      • 1. 错误类型不匹配
      • 2. 过度使用 ?
      • 3. panic! 在生产环境
    • 🎯 实战案例
      • 案例 1:配置文件加载器
      • 案例 2:用户输入验证
      • 案例 3:批量文件处理器
    • 🧠 思维导图
    • 📝 小结
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档