iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Rust

30天Rust從零到全端系列 第 13

Day 13: 錯誤處理基礎

  • 分享至 

  • xImage
  •  

前言

在昨天學習了列舉和模式匹配之後,今天我們要來看看 Rust 最重要的特性之一 - 錯誤處理。Rust 通過型別系統強制你考慮和處理可能出現的錯誤讓程式更加穩定和可靠。不像其他語言可能會拋出異常或返回特殊值,Rust 使用 ResultOption 型別來明確表示操作可能失敗。

但為什麼 Rust 的錯誤處理特殊?

在其他語言中,錯誤處理通常是這樣的:

// 類似 Java/C# 的方式(Rust 不支援)
// try {
//     let file = open_file("data.txt");
//     let content = file.read();
// } catch (IOException e) {
//     println!("錯誤: {}", e);
// }

// 或類似 C 的方式(容易忽略錯誤)
// FILE* file = fopen("data.txt", "r");
// if (file == NULL) {  // 容易忘記檢查!
//     // 錯誤處理
// }

Rust 的方式:

use std::fs;

fn read_file_content() -> Result<String, std::io::Error> {
    fs::read_to_string("data.txt")
}

fn main() {
    match read_file_content() {
        Ok(content) => println!("檔案內容: {}", content),
        Err(error) => println!("讀取失敗: {}", error),
    }
}

關鍵差異:

  • 強制處理:你必須處理 Result,否則編譯器會警告
  • 型別安全:錯誤型別在編譯時就確定
  • 無異常:沒有隱藏的控制流,所有錯誤都是明確的

Result 型別深入解析

Result 的定義

enum Result<T, E> {
    Ok(T),      // 成功,包含結果值
    Err(E),     // 失敗,包含錯誤值
}

基本使用方式

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("除零錯誤"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    // 方式 1:使用 match
    match divide(10.0, 2.0) {
        Ok(result) => println!("結果: {}", result),
        Err(error) => println!("錯誤: {}", error),
    }
    
    // 方式 2:使用 if let
    if let Ok(result) = divide(10.0, 3.0) {
        println!("成功計算: {}", result);
    }
    
    // 方式 3:使用方法
    let result = divide(15.0, 3.0)
        .unwrap_or(0.0);  // 錯誤時使用預設值
    println!("結果或預設值: {}", result);
}

錯誤處理的方法

unwrap 和 expect

fn demo_unwrap() {
    let good_result = divide(10.0, 2.0);
    let value = good_result.unwrap();  // 確定不會出錯時使用
    println!("值: {}", value);
    
    // 提供更好的錯誤訊息
    let value = divide(10.0, 2.0)
        .expect("這個計算不應該失敗");
    println!("值: {}", value);
    
    // 危險!如果結果是 Err,程式會 panic
    // let bad_value = divide(10.0, 0.0).unwrap();  // 會 panic
}

unwrap_or 和 unwrap_or_else

fn demo_unwrap_or() {
    // 提供預設值
    let result1 = divide(10.0, 0.0)
        .unwrap_or(-1.0);
    println!("結果或預設值: {}", result1);
    
    // 使用閉包提供預設值
    let result2 = divide(10.0, 0.0)
        .unwrap_or_else(|err| {
            println!("計算失敗: {}", err);
            0.0  // 回傳預設值
        });
    println!("結果或計算的預設值: {}", result2);
}

map 和 map_err

fn demo_map() {
    // 對成功結果進行轉換
    let result = divide(10.0, 2.0)
        .map(|x| x * 2.0)  // 如果成功,結果乘以 2
        .map(|x| format!("答案是: {}", x));  // 轉換成字串
    
    match result {
        Ok(msg) => println!("{}", msg),
        Err(e) => println!("錯誤: {}", e),
    }
    
    // 對錯誤進行轉換
    let result = divide(10.0, 0.0)
        .map_err(|e| format!("數學錯誤: {}", e));  // 轉換錯誤型別
    
    match result {
        Ok(value) => println!("值: {}", value),
        Err(e) => println!("{}", e),
    }
}

問號運算子(?)

問號運算子是 Rust 錯誤處理的核心特性,它讓錯誤傳播變得簡潔:

基本用法

use std::fs;
use std::io;

// 不使用 ? 運算子(繁瑣)
fn read_username_from_file_verbose() -> Result<String, io::Error> {
    let username_file_result = fs::read_to_string("username.txt");
    
    let username = match username_file_result {
        Ok(content) => content,
        Err(e) => return Err(e),
    };
    
    Ok(username.trim().to_string())
}

// 使用 ? 運算子(簡潔)
fn read_username_from_file() -> Result<String, io::Error> {
    let username = fs::read_to_string("username.txt")?;
    Ok(username.trim().to_string())
}

// 更簡潔的版本
fn read_username_simple() -> Result<String, io::Error> {
    Ok(fs::read_to_string("username.txt")?.trim().to_string())
}

? 運算子的工作原理

// ? 運算子等同於這個 match
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
    // 使用 ?
    let num = s.parse::<i32>()?;
    Ok(num * 2)
}

// 等同於:
fn parse_number_expanded(s: &str) -> Result<i32, std::num::ParseIntError> {
    let num = match s.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok(num * 2)
}

自定義錯誤型別

簡單的自定義錯誤

#[derive(Debug)]
enum MathError {
    DivisionByZero,
    NegativeSquareRoot,
    Overflow,
}

impl std::fmt::Display for MathError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            MathError::DivisionByZero => write!(f, "除零錯誤"),
            MathError::NegativeSquareRoot => write!(f, "負數不能開平方根"),
            MathError::Overflow => write!(f, "數值溢位"),
        }
    }
}

impl std::error::Error for MathError {}

fn safe_divide(a: f64, b: f64) -> Result<f64, MathError> {
    if b == 0.0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(a / b)
    }
}

fn safe_sqrt(x: f64) -> Result<f64, MathError> {
    if x < 0.0 {
        Err(MathError::NegativeSquareRoot)
    } else {
        Ok(x.sqrt())
    }
}

複合錯誤處理

fn complex_calculation(a: f64, b: f64, c: f64) -> Result<f64, MathError> {
    let quotient = safe_divide(a, b)?;
    let sum = quotient + c;
    let result = safe_sqrt(sum)?;
    Ok(result)
}

fn main() {
    match complex_calculation(16.0, 2.0, 9.0) {
        Ok(result) => println!("計算結果: {}", result),
        Err(e) => println!("計算失敗: {}", e),
    }
    
    // 測試不同的錯誤情況
    match complex_calculation(10.0, 0.0, 5.0) {
        Ok(result) => println!("結果: {}", result),
        Err(MathError::DivisionByZero) => println!("出現除零錯誤!"),
        Err(e) => println!("其他錯誤: {}", e),
    }
}

實戰:任務管理系統的錯誤處理

讓我們為任務管理系統添加完整的錯誤處理:

use std::collections::HashMap;
use std::fmt;

#[derive(Debug, Clone)]
pub enum TaskError {
    NotFound(u32),
    AlreadyExists(String),
    InvalidStatus { 
        current: String, 
        requested: String 
    },
    InvalidPriority(i32),
    EmptyTitle,
    DatabaseError(String),
}

impl fmt::Display for TaskError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            TaskError::NotFound(id) => write!(f, "找不到 ID {} 的任務", id),
            TaskError::AlreadyExists(title) => write!(f, "任務 '{}' 已經存在", title),
            TaskError::InvalidStatus { current, requested } => {
                write!(f, "無法從 '{}' 狀態變更為 '{}'", current, requested)
            },
            TaskError::InvalidPriority(p) => write!(f, "無效的優先順序: {}", p),
            TaskError::EmptyTitle => write!(f, "任務標題不能為空"),
            TaskError::DatabaseError(msg) => write!(f, "資料庫錯誤: {}", msg),
        }
    }
}

impl std::error::Error for TaskError {}

#[derive(Debug, Clone, PartialEq)]
pub enum TaskStatus {
    Todo,
    InProgress,
    Done,
    Cancelled,
}

impl fmt::Display for TaskStatus {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            TaskStatus::Todo => write!(f, "待辦"),
            TaskStatus::InProgress => write!(f, "進行中"),
            TaskStatus::Done => write!(f, "已完成"),
            TaskStatus::Cancelled => write!(f, "已取消"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Task {
    pub id: u32,
    pub title: String,
    pub description: String,
    pub status: TaskStatus,
    pub priority: u32,
}

impl Task {
    pub fn new(id: u32, title: String, description: String) -> Result<Self, TaskError> {
        if title.trim().is_empty() {
            return Err(TaskError::EmptyTitle);
        }
        
        Ok(Task {
            id,
            title: title.trim().to_string(),
            description,
            status: TaskStatus::Todo,
            priority: 1,
        })
    }
    
    pub fn set_priority(&mut self, priority: u32) -> Result<(), TaskError> {
        if priority == 0 || priority > 5 {
            Err(TaskError::InvalidPriority(priority as i32))
        } else {
            self.priority = priority;
            Ok(())
        }
    }
    
    pub fn change_status(&mut self, new_status: TaskStatus) -> Result<(), TaskError> {
        let valid_transition = match (&self.status, &new_status) {
            (TaskStatus::Todo, TaskStatus::InProgress) => true,
            (TaskStatus::Todo, TaskStatus::Cancelled) => true,
            (TaskStatus::InProgress, TaskStatus::Done) => true,
            (TaskStatus::InProgress, TaskStatus::Todo) => true,
            (TaskStatus::InProgress, TaskStatus::Cancelled) => true,
            (current, new) if current == new => true,
            _ => false,
        };
        
        if valid_transition {
            self.status = new_status;
            Ok(())
        } else {
            Err(TaskError::InvalidStatus {
                current: self.status.to_string(),
                requested: new_status.to_string(),
            })
        }
    }
}

pub struct TaskManager {
    tasks: HashMap<u32, Task>,
    next_id: u32,
    titles: std::collections::HashSet<String>,
}

impl TaskManager {
    pub fn new() -> Self {
        TaskManager {
            tasks: HashMap::new(),
            next_id: 1,
            titles: std::collections::HashSet::new(),
        }
    }
    
    pub fn create_task(&mut self, title: String, description: String) -> Result<u32, TaskError> {
        // 檢查標題是否已存在
        if self.titles.contains(&title) {
            return Err(TaskError::AlreadyExists(title));
        }
        
        let task = Task::new(self.next_id, title.clone(), description)?;
        let id = task.id;
        
        self.tasks.insert(id, task);
        self.titles.insert(title);
        self.next_id += 1;
        
        Ok(id)
    }
    
    pub fn get_task(&self, id: u32) -> Result<&Task, TaskError> {
        self.tasks.get(&id).ok_or(TaskError::NotFound(id))
    }
    
    pub fn get_task_mut(&mut self, id: u32) -> Result<&mut Task, TaskError> {
        self.tasks.get_mut(&id).ok_or(TaskError::NotFound(id))
    }
    
    pub fn update_task_status(&mut self, id: u32, status: TaskStatus) -> Result<(), TaskError> {
        let task = self.get_task_mut(id)?;
        task.change_status(status)?;
        Ok(())
    }
    
    pub fn set_task_priority(&mut self, id: u32, priority: u32) -> Result<(), TaskError> {
        let task = self.get_task_mut(id)?;
        task.set_priority(priority)?;
        Ok(())
    }
    
    pub fn delete_task(&mut self, id: u32) -> Result<Task, TaskError> {
        let task = self.tasks.remove(&id).ok_or(TaskError::NotFound(id))?;
        self.titles.remove(&task.title);
        Ok(task)
    }
    
    pub fn list_tasks(&self) -> Vec<&Task> {
        self.tasks.values().collect()
    }
    
    pub fn list_tasks_by_status(&self, status: TaskStatus) -> Vec<&Task> {
        self.tasks.values()
            .filter(|task| task.status == status)
            .collect()
    }
    
    // 批次操作,展示錯誤收集
    pub fn complete_multiple_tasks(&mut self, ids: Vec<u32>) -> Result<Vec<u32>, Vec<(u32, TaskError)>> {
        let mut completed = Vec::new();
        let mut errors = Vec::new();
        
        for id in ids {
            match self.update_task_status(id, TaskStatus::Done) {
                Ok(()) => completed.push(id),
                Err(e) => errors.push((id, e)),
            }
        }
        
        if errors.is_empty() {
            Ok(completed)
        } else {
            Err(errors)
        }
    }
    
    // 示範複合操作的錯誤處理
    pub fn create_and_start_task(&mut self, title: String, description: String, priority: u32) -> Result<u32, TaskError> {
        let id = self.create_task(title, description)?;
        self.set_task_priority(id, priority)?;
        self.update_task_status(id, TaskStatus::InProgress)?;
        Ok(id)
    }
}

// 輔助函式:安全地解析優先順序
pub fn parse_priority(input: &str) -> Result<u32, TaskError> {
    input.parse()
        .map_err(|_| TaskError::InvalidPriority(-1))
        .and_then(|p| {
            if p >= 1 && p <= 5 {
                Ok(p)
            } else {
                Err(TaskError::InvalidPriority(p as i32))
            }
        })
}

// 輔助函式:安全地解析狀態
pub fn parse_status(input: &str) -> Result<TaskStatus, TaskError> {
    match input.to_lowercase().as_str() {
        "todo" | "待辦" => Ok(TaskStatus::Todo),
        "progress" | "進行中" => Ok(TaskStatus::InProgress),
        "done" | "完成" => Ok(TaskStatus::Done),
        "cancelled" | "取消" => Ok(TaskStatus::Cancelled),
        _ => Err(TaskError::InvalidStatus {
            current: "unknown".to_string(),
            requested: input.to_string(),
        }),
    }
}

使用帶有錯誤處理的任務管理系統

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut manager = TaskManager::new();
    
    // 建立任務,展示成功路徑
    println!("=== 建立任務 ===");
    match manager.create_task(
        String::from("學習 Rust 錯誤處理"),
        String::from("深入理解 Result 和錯誤處理模式")
    ) {
        Ok(id) => println!("✓ 成功建立任務 ID: {}", id),
        Err(e) => println!("✗ 建立任務失敗: {}", e),
    }
    
    // 嘗試建立重複標題的任務
    match manager.create_task(
        String::from("學習 Rust 錯誤處理"),  // 重複標題
        String::from("重複的任務")
    ) {
        Ok(id) => println!("✓ 成功建立任務 ID: {}", id),
        Err(e) => println!("✗ 預期的錯誤: {}", e),
    }
    
    // 建立更多任務用於測試
    let task_ids = vec![
        manager.create_task(String::from("實作專案"), String::from("建立完整專案"))?,
        manager.create_task(String::from("寫測試"), String::from("編寫單元測試"))?,
        manager.create_task(String::from("文檔撰寫"), String::from("撰寫使用者文檔"))?,
    ];
    
    println!("\n=== 設定任務屬性 ===");
    // 設定優先順序
    for (i, &id) in task_ids.iter().enumerate() {
        let priority = (i % 5) + 1;  // 1-5
        match manager.set_task_priority(id, priority as u32) {
            Ok(()) => println!("✓ 任務 {} 優先順序設為 {}", id, priority),
            Err(e) => println!("✗ 設定失敗: {}", e),
        }
    }
    
    // 測試無效優先順序
    match manager.set_task_priority(task_ids[0], 10) {  // 無效值
        Ok(()) => println!("✓ 設定成功"),
        Err(e) => println!("✗ 預期的錯誤: {}", e),
    }
    
    println!("\n=== 狀態變更 ===");
    // 開始第一個任務
    match manager.update_task_status(task_ids[0], TaskStatus::InProgress) {
        Ok(()) => println!("✓ 任務 {} 已開始", task_ids[0]),
        Err(e) => println!("✗ 狀態變更失敗: {}", e),
    }
    
    // 嘗試不合法的狀態變更
    match manager.update_task_status(task_ids[1], TaskStatus::Done) {  // 從待辦直接到完成
        Ok(()) => println!("✓ 狀態變更成功"),
        Err(e) => println!("✗ 預期的錯誤: {}", e),
    }
    
    println!("\n=== 複合操作 ===");
    // 使用複合操作
    match manager.create_and_start_task(
        String::from("緊急任務"),
        String::from("需要立即處理的任務"),
        5
    ) {
        Ok(id) => println!("✓ 緊急任務建立並開始,ID: {}", id),
        Err(e) => println!("✗ 複合操作失敗: {}", e),
    }
    
    println!("\n=== 批次操作 ===");
    // 批次完成任務(先將它們設為進行中)
    for &id in &task_ids[1..] {
        let _ = manager.update_task_status(id, TaskStatus::InProgress);
    }
    
    match manager.complete_multiple_tasks(task_ids.clone()) {
        Ok(completed) => println!("✓ 完成了 {} 個任務: {:?}", completed.len(), completed),
        Err(errors) => {
            println!("✗ 批次操作部分失敗:");
            for (id, error) in errors {
                println!("  任務 {}: {}", id, error);
            }
        }
    }
    
    println!("\n=== 任務列表 ===");
    // 列出所有任務
    let tasks = manager.list_tasks();
    for task in tasks {
        println!("任務 {}: {} [{}] (優先順序: {})", 
                 task.id, task.title, task.status, task.priority);
    }
    
    println!("\n=== 輔助函式測試 ===");
    // 測試輔助函式
    match parse_priority("3") {
        Ok(p) => println!("✓ 解析優先順序: {}", p),
        Err(e) => println!("✗ 解析失敗: {}", e),
    }
    
    match parse_priority("invalid") {
        Ok(p) => println!("✓ 解析優先順序: {}", p),
        Err(e) => println!("✗ 預期的錯誤: {}", e),
    }
    
    match parse_status("progress") {
        Ok(status) => println!("✓ 解析狀態: {}", status),
        Err(e) => println!("✗ 解析失敗: {}", e),
    }
    
    Ok(())
}

錯誤處理小tips

1. 選擇合適的錯誤處理策略

// 快速失敗:使用 unwrap/expect(確定不會失敗時)
let config_value = std::env::var("CONFIG_PATH")
    .expect("CONFIG_PATH 環境變數必須設定");

// 提供預設值:使用 unwrap_or
let timeout = std::env::var("TIMEOUT")
    .unwrap_or_else(|_| "30".to_string())
    .parse()
    .unwrap_or(30);

// 傳播錯誤:使用 ?
fn load_config() -> Result<Config, ConfigError> {
    let path = std::env::var("CONFIG_PATH")?;
    let content = std::fs::read_to_string(path)?;
    let config: Config = serde_json::from_str(&content)?;
    Ok(config)
}

2. 錯誤型別設計

// 好的錯誤型別設計
#[derive(Debug)]
pub enum ApplicationError {
    Io(std::io::Error),
    Parse(serde_json::Error),
    Validation(String),
    Network { 
        url: String, 
        status_code: u16 
    },
}

// 實作 From 特徵以支援 ? 運算子
impl From<std::io::Error> for ApplicationError {
    fn from(error: std::io::Error) -> Self {
        ApplicationError::Io(error)
    }
}

impl From<serde_json::Error> for ApplicationError {
    fn from(error: serde_json::Error) -> Self {
        ApplicationError::Parse(error)
    }
}

3. 使用 anyhow 和 thiserror crates

# Cargo.toml
[dependencies]
anyhow = "1.0"
thiserror = "1.0"
use anyhow::{Context, Result};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("資料驗證失敗")]
    Validation(#[from] ValidationError),
    
    #[error("網路錯誤")]
    Network(#[from] reqwest::Error),
    
    #[error("找不到 ID 為 {id} 的項目")]
    NotFound { id: u64 },
    
    #[error("資料庫連線失敗")]
    Database(#[from] sqlx::Error),
}

fn process_data(id: u64) -> Result<String> {
    let data = fetch_data(id)
        .with_context(|| format!("無法取得 ID {} 的資料", id))?;
    
    let processed = validate_and_process(&data)
        .context("資料處理失敗")?;
    
    Ok(processed)
}

Follow-up

1. 基礎練習:安全的計算器

#[derive(Debug)]
enum CalculatorError {
    DivisionByZero,
    InvalidOperation,
    Overflow,
}

// 實作以下函式,返回適當的 Result:
// - add(a: f64, b: f64) -> Result<f64, CalculatorError>
// - subtract(a: f64, b: f64) -> Result<f64, CalculatorError>
// - multiply(a: f64, b: f64) -> Result<f64, CalculatorError>
// - divide(a: f64, b: f64) -> Result<f64, CalculatorError>
// - parse_and_calculate(expression: &str) -> Result<f64, CalculatorError>

2. 進階練習:檔案處理器

use std::fs;
use std::io;

#[derive(Debug)]
enum FileProcessorError {
    IoError(io::Error),
    InvalidFormat,
    EmptyFile,
}

impl From<io::Error> for FileProcessorError {
    fn from(error: io::Error) -> Self {
        FileProcessorError::IoError(error)
    }
}

// 實作以下功能:
// - read_numbers_from_file(path: &str) -> Result<Vec<i32>, FileProcessorError>
// - calculate_statistics(numbers: &[i32]) -> Result<Statistics, FileProcessorError>
// - write_statistics_to_file(stats: &Statistics, path: &str) -> Result<(), FileProcessorError>

struct Statistics {
    count: usize,
    sum: i64,
    average: f64,
    min: i32,
    max: i32,
}

3. 挑戰練習:用戶管理系統

// 建立一個完整的用戶管理系統,包含:
// - 自定義錯誤型別
// - 用戶註冊、登入、更新資料功能
// - 密碼驗證
// - 檔案持久化
// - 完整的錯誤處理

#[derive(Debug)]
pub enum UserError {
    AlreadyExists(String),
    NotFound(String),
    InvalidPassword,
    WeakPassword(String),
    InvalidEmail(String),
    StorageError(String),
}

常見錯誤和陷阱

1. 過度使用 unwrap

// 不好:容易 panic
fn bad_example() {
    let content = std::fs::read_to_string("config.txt").unwrap();
    let number: i32 = content.parse().unwrap();
}

// 好:適當的錯誤處理
fn good_example() -> Result<i32, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("config.txt")?;
    let number: i32 = content.parse()?;
    Ok(number)
}

2. 忽略錯誤

// 不好:忽略錯誤
fn bad_example() {
    let _ = std::fs::write("output.txt", "data");  // 錯誤被忽略
}

// 好:處理或傳播錯誤
fn good_example() -> Result<(), std::io::Error> {
    std::fs::write("output.txt", "data")?;
    Ok(())
}

3. 錯誤型別設計不當

// 不好:使用 String 作為錯誤型別
fn bad_error() -> Result<i32, String> {
    Err("Something went wrong".to_string())
}

// 好:使用專門的錯誤型別
#[derive(Debug)]
enum MyError {
    ParseError,
    NetworkError,
}

fn good_error() -> Result<i32, MyError> {
    Err(MyError::ParseError)
}

重點回顧

  • ✅ Rust 使用 Result<T, E> 明確處理可能失敗的操作
  • ? 運算子簡化錯誤傳播,讓程式碼更簡潔
  • ✅ 自定義錯誤型別提供更好的錯誤資訊和處理
  • unwrapexpect 應該謹慎使用,主要用於確定不會失敗的情況
  • unwrap_ormapand_then 等方法提供函式式錯誤處理
  • ✅ 實作 From 特徵讓錯誤型別轉換更方便
  • ✅ 良好的錯誤處理讓程式更穩定和可維護
  • ✅ 七夕吃甚麼

上一篇
Day 12: 列舉與模式匹配:表達多種可能性
下一篇
Day 14: 週末專案 - CLI 待辦事項應用
系列文
30天Rust從零到全端15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言