iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0
Rust

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

Day 7: 第一個完整專案 - 終端機任務管理器

  • 分享至 

  • xImage
  •  

前言

Hi 大家,經過前幾天的學習,今天我們要整合所有學到的概念,建立第一個完整的 Rust 專案:終端機任務管理器。這個專案會運用到變數、函式、控制流程、迭代器等所有概念,體驗完整的 Rust 開發流程。

專案規劃

功能需求

  1. 新增任務
  2. 列出所有任務
  3. 標記任務完成
  4. 刪除任務
  5. 搜尋任務
  6. 匯出任務清單

專案建置與執行

建立專案

# 建立新專案
cargo new task_manager
cd task_manager

# 建立檔案結構
mkdir src
touch src/task.rs src/utils.rs

# 編譯專案
cargo build

# 執行專案
cargo run

專案結構

task_manager/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── task.rs
│   └── utils.rs

完整專案實作

Cargo.toml

[package]
name = "task_manager"
version = "0.1.0"
edition = "2024"

[dependencies]

src/task.rs

use std::fmt;

#[derive(Debug, Clone)]
pub struct Task {
    pub id: usize,
    pub title: String,
    pub description: String,
    pub completed: bool,
    pub priority: Priority,
    pub tags: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Priority {
    Low,
    Medium,
    High,
    Urgent,
}

impl fmt::Display for Priority {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let priority_str = match self {
            Priority::Low => "低",
            Priority::Medium => "中",
            Priority::High => "高",
            Priority::Urgent => "緊急",
        };
        write!(f, "{}", priority_str)
    }
}

impl Task {
    pub fn new(id: usize, title: String, description: String, priority: Priority) -> Self {
        Task {
            id,
            title,
            description,
            completed: false,
            priority,
            tags: Vec::new(),
        }
    }
    
    pub fn toggle_completion(&mut self) {
        self.completed = !self.completed;
    }
    
    pub fn add_tag(&mut self, tag: String) {
        if !self.tags.contains(&tag) {
            self.tags.push(tag);
        }
    }
    
    pub fn matches_keyword(&self, keyword: &str) -> bool {
        let keyword = keyword.to_lowercase();
        self.title.to_lowercase().contains(&keyword) ||
        self.description.to_lowercase().contains(&keyword) ||
        self.tags.iter().any(|tag| tag.to_lowercase().contains(&keyword))
    }
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let status = if self.completed { "✓" } else { "○" };
        let tags_str = if self.tags.is_empty() {
            String::new()
        } else {
            format!(" [{}]", self.tags.join(", "))
        };
        
        write!(f, "{} [{}] {} - {} (優先度: {}){}", 
               status, self.id, self.title, self.description, self.priority, tags_str)
    }
}

src/utils.ts

use std::io;

pub fn get_user_input(prompt: &str) -> String {
    println!("{}", prompt);
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("讀取輸入失敗");
    input.trim().to_string()
}

pub fn get_number_input(prompt: &str) -> Option<usize> {
    let input = get_user_input(prompt);
    input.parse().ok()
}

pub fn confirm_action(prompt: &str) -> bool {
    loop {
        let input = get_user_input(&format!("{} (y/n)", prompt));
        match input.to_lowercase().as_str() {
            "y" | "yes" | "是" => return true,
            "n" | "no" | "否" => return false,
            _ => println!("請輸入 y 或 n"),
        }
    }
}

pub fn clear_screen() {
    print!("\x1B[2J\x1B[1;1H");
}

src/main.rs

mod task;
mod utils;

use task::{Task, Priority};
use utils::{get_user_input, get_number_input, confirm_action, clear_screen};
use std::collections::HashMap;

struct TaskManager {
    tasks: Vec<Task>,
    next_id: usize,
}

impl TaskManager {
    fn new() -> Self {
        TaskManager {
            tasks: Vec::new(),
            next_id: 1,
        }
    }
    
    fn add_task(&mut self) {
        println!("\n=== 新增任務 ===");
        
        let title = get_user_input("任務標題:");
        if title.is_empty() {
            println!("標題不能為空!");
            return;
        }
        
        let description = get_user_input("任務描述:");
        
        let priority = loop {
            println!("選擇優先度:");
            println!("1. 低");
            println!("2. 中");
            println!("3. 高");
            println!("4. 緊急");
            
            match get_number_input("請選擇 (1-4):") {
                Some(1) => break Priority::Low,
                Some(2) => break Priority::Medium,
                Some(3) => break Priority::High,
                Some(4) => break Priority::Urgent,
                _ => println!("請輸入 1-4"),
            }
        };
        
        let mut task = Task::new(self.next_id, title, description, priority);
        
        // 新增標籤
        let tags_input = get_user_input("標籤 (用逗號分隔,可留空):");
        if !tags_input.is_empty() {
            for tag in tags_input.split(',') {
                let tag = tag.trim().to_string();
                if !tag.is_empty() {
                    task.add_tag(tag);
                }
            }
        }
        
        self.tasks.push(task);
        self.next_id += 1;
        
        println!("✓ 任務新增成功!");
    }
    
    fn list_tasks(&self) {
        println!("\n=== 任務清單 ===");
        
        if self.tasks.is_empty() {
            println!("目前沒有任務");
            return;
        }
        
        // 按優先度和完成狀態分組顯示
        let incomplete_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|task| !task.completed)
            .collect();
            
        let completed_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|task| task.completed)
            .collect();
        
        if !incomplete_tasks.is_empty() {
            println!("\n📋 待辦任務:");
            for task in incomplete_tasks {
                println!("  {}", task);
            }
        }
        
        if !completed_tasks.is_empty() {
            println!("\n✅ 已完成任務:");
            for task in completed_tasks {
                println!("  {}", task);
            }
        }
        
        self.show_statistics();
    }
    
    fn show_statistics(&self) {
        let total = self.tasks.len();
        let completed = self.tasks.iter().filter(|task| task.completed).count();
        let completion_rate = if total > 0 {
            (completed as f64 / total as f64) * 100.0
        } else {
            0.0
        };
        
        println!("\n📊 統計資訊:");
        println!("  總任務數: {}", total);
        println!("  已完成: {}", completed);
        println!("  完成率: {:.1}%", completion_rate);
        
        // 優先度統計
        let priority_stats = self.tasks
            .iter()
            .filter(|task| !task.completed)
            .fold(HashMap::new(), |mut acc, task| {
                *acc.entry(&task.priority).or_insert(0) += 1;
                acc
            });
        
        if !priority_stats.is_empty() {
            println!("  待辦任務優先度分布:");
            for (priority, count) in priority_stats {
                println!("    {}: {} 個", priority, count);
            }
        }
    }
    
    fn toggle_task_completion(&mut self) {
        if self.tasks.is_empty() {
            println!("沒有任務可以操作");
            return;
        }
        
        self.list_tasks();
        
        if let Some(id) = get_number_input("\n請輸入要切換狀態的任務 ID:") {
            if let Some(task) = self.tasks.iter_mut().find(|task| task.id == id) {
                task.toggle_completion();
                let status = if task.completed { "完成" } else { "未完成" };
                println!("✓ 任務 '{}' 已標記為{}", task.title, status);
            } else {
                println!("找不到 ID 為 {} 的任務", id);
            }
        }
    }
    
    fn delete_task(&mut self) {
        if self.tasks.is_empty() {
            println!("沒有任務可以刪除");
            return;
        }
        
        self.list_tasks();
        
        if let Some(id) = get_number_input("\n請輸入要刪除的任務 ID:") {
            if let Some(pos) = self.tasks.iter().position(|task| task.id == id) {
                let task = &self.tasks[pos];
                if confirm_action(&format!("確定要刪除任務 '{}'?", task.title)) {
                    let removed_task = self.tasks.remove(pos);
                    println!("✓ 已刪除任務: {}", removed_task.title);
                }
            } else {
                println!("找不到 ID 為 {} 的任務", id);
            }
        }
    }
    
    fn search_tasks(&self) {
        let keyword = get_user_input("請輸入搜尋關鍵字:");
        
        if keyword.is_empty() {
            println!("關鍵字不能為空");
            return;
        }
        
        let matching_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|task| task.matches_keyword(&keyword))
            .collect();
        
        println!("\n=== 搜尋結果: '{}' ===", keyword);
        
        if matching_tasks.is_empty() {
            println!("沒有找到相關任務");
        } else {
            println!("找到 {} 個相關任務:", matching_tasks.len());
            for task in matching_tasks {
                println!("  {}", task);
            }
        }
    }
    
    fn export_tasks(&self) {
        if self.tasks.is_empty() {
            println!("沒有任務可以匯出");
            return;
        }
        
        println!("\n=== 匯出任務清單 ===");
        println!("# 任務清單報告\n");
        
        // 按優先度分組
        let priority_groups = [
            (Priority::Urgent, "🔥 緊急任務"),
            (Priority::High, "⚡ 高優先度任務"),
            (Priority::Medium, "📋 中優先度任務"),
            (Priority::Low, "📝 低優先度任務"),
        ];
        
        for (priority, title) in priority_groups {
            let tasks_in_priority: Vec<&Task> = self.tasks
                .iter()
                .filter(|task| task.priority == priority && !task.completed)
                .collect();
            
            if !tasks_in_priority.is_empty() {
                println!("## {}", title);
                for task in tasks_in_priority {
                    println!("- [{}] {} - {}", 
                        if task.completed { "x" } else { " " },
                        task.title, 
                        task.description
                    );
                    if !task.tags.is_empty() {
                        println!("  標籤: {}", task.tags.join(", "));
                    }
                }
                println!();
            }
        }
        
        // 已完成任務
        let completed_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|task| task.completed)
            .collect();
        
        if !completed_tasks.is_empty() {
            println!("## ✅ 已完成任務");
            for task in completed_tasks {
                println!("- [x] {} - {}", task.title, task.description);
            }
        }
        
        self.show_statistics();
    }
}

fn main() {
    let mut task_manager = TaskManager::new();
    
    // 新增一些範例任務
    task_manager.tasks.push(Task::new(
        1, 
        "學習 Rust".to_string(), 
        "完成 30 天 Rust 教學".to_string(), 
        Priority::High
    ));
    task_manager.tasks[0].add_tag("程式設計".to_string());
    task_manager.tasks[0].add_tag("學習".to_string());
    
    task_manager.next_id = 2;
    
    println!("🦀 歡迎使用 Rust 任務管理器!");
    
    loop {
        println!("\n{}", "=".repeat(40));
        println!("📋 任務管理器主選單");
        println!("{}", "=".repeat(40));
        println!("1. 📝 新增任務");
        println!("2. 📋 列出任務");
        println!("3. ✅ 切換任務狀態");
        println!("4. 🗑️  刪除任務");
        println!("5. 🔍 搜尋任務");
        println!("6. 📤 匯出任務清單");
        println!("7. 🧹 清理已完成任務");
        println!("8. 🚪 離開程式");
        
        match get_number_input("請選擇操作 (1-8):") {
            Some(1) => task_manager.add_task(),
            Some(2) => task_manager.list_tasks(),
            Some(3) => task_manager.toggle_task_completion(),
            Some(4) => task_manager.delete_task(),
            Some(5) => task_manager.search_tasks(),
            Some(6) => task_manager.export_tasks(),
            Some(7) => cleanup_completed_tasks(&mut task_manager),
            Some(8) => {
                if confirm_action("確定要離開嗎?") {
                    println!("感謝使用任務管理器!👋");
                    break;
                }
            }
            _ => {
                println!("❌ 無效選擇,請輸入 1-8");
                continue;
            }
        }
        
        // 暫停讓使用者查看結果
        if get_user_input("\n按 Enter 繼續...").is_empty() || true {
            clear_screen();
        }
    }
}

fn cleanup_completed_tasks(task_manager: &mut TaskManager) {
    let completed_count = task_manager.tasks
        .iter()
        .filter(|task| task.completed)
        .count();
    
    if completed_count == 0 {
        println!("沒有已完成的任務需要清理");
        return;
    }
    
    if confirm_action(&format!("確定要刪除 {} 個已完成的任務嗎?", completed_count)) {
        task_manager.tasks.retain(|task| !task.completed);
        println!("✓ 已清理 {} 個已完成任務", completed_count);
    }
}

進階功能練習

任務過濾器

impl TaskManager {
    fn filter_tasks_by_priority(&self, priority: Priority) {
        let filtered_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|task| task.priority == priority && !task.completed)
            .collect();
        
        println!("\n=== {} 優先度任務 ===", priority);
        
        if filtered_tasks.is_empty() {
            println!("沒有符合條件的任務");
        } else {
            for task in filtered_tasks {
                println!("  {}", task);
            }
        }
    }
    
    fn show_tasks_by_tag(&self) {
        let tag = get_user_input("請輸入標籤名稱:");
        
        let tagged_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|task| task.tags.iter().any(|t| t.contains(&tag)))
            .collect();
        
        println!("\n=== 標籤 '{}' 的任務 ===", tag);
        
        if tagged_tasks.is_empty() {
            println!("沒有找到相關標籤的任務");
        } else {
            for task in tagged_tasks {
                println!("  {}", task);
            }
        }
    }
}

測試功能

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_task_creation() {
        let task = Task::new(
            1, 
            "測試任務".to_string(), 
            "這是一個測試".to_string(), 
            Priority::Medium
        );
        
        assert_eq!(task.id, 1);
        assert_eq!(task.title, "測試任務");
        assert!(!task.completed);
    }
    
    #[test]
    fn test_task_completion() {
        let mut task = Task::new(
            1, 
            "測試".to_string(), 
            "描述".to_string(), 
            Priority::Low
        );
        
        assert!(!task.completed);
        task.toggle_completion();
        assert!(task.completed);
    }
    
    #[test]
    fn test_task_search() {
        let task = Task::new(
            1, 
            "學習 Rust".to_string(), 
            "程式設計學習".to_string(), 
            Priority::High
        );
        
        assert!(task.matches_keyword("rust"));
        assert!(task.matches_keyword("程式"));
        assert!(!task.matches_keyword("java"));
    }
}

Follow-up

挑戰 1:任務排序

實作任務按不同條件排序的功能:

  • 按優先度排序
  • 按創建時間排序
  • 按標題字母順序排序

挑戰 2:任務統計

新增更詳細的統計功能:

  • 每日完成任務數
  • 平均完成時間
  • 最常用的標籤

挑戰 3:檔案持久化

將任務資料儲存到檔案:

  • JSON 格式匯出/匯入
  • 程式重啟後保持資料

除錯小技巧

常見問題與解決方案

// 問題 1: 借用檢查器錯誤
// 錯誤寫法
fn bad_example(tasks: &mut Vec<Task>) {
    for task in tasks.iter() {
        if task.completed {
            tasks.remove(0); // 錯誤:在借用期間修改
        }
    }
}

// 正確寫法
fn good_example(tasks: &mut Vec<Task>) {
    tasks.retain(|task| !task.completed);
}

// 問題 2: 字串所有權
// 錯誤寫法
fn bad_string_handling() -> String {
    let s = "Hello".to_string();
    s.as_str() // 錯誤:回傳借用而非擁有
}

// 正確寫法
fn good_string_handling() -> String {
    let s = "Hello".to_string();
    s // 回傳所有權
}

效能優化小技巧

迭代器鏈式操作

impl TaskManager {
    fn get_urgent_incomplete_tasks(&self) -> Vec<&Task> {
        self.tasks
            .iter()
            .filter(|task| !task.completed)
            .filter(|task| task.priority == Priority::Urgent)
            .collect()
    }
    
    fn count_tasks_by_status(&self) -> (usize, usize) {
        self.tasks
            .iter()
            .fold((0, 0), |(completed, incomplete), task| {
                if task.completed {
                    (completed + 1, incomplete)
                } else {
                    (completed, incomplete + 1)
                }
            })
    }
}

總結

恭喜你完成了第一週的學習!

目前學到的

  • Day 1-2: 環境建置和 Cargo 工具鏈
  • Day 3-4: 變數、資料型別、函式基礎
  • Day 5: 進階控制流程和錯誤處理
  • Day 6: 迭代器系統和進階模式匹配
  • Day 7: 完整專案實作

核心概念

  1. 函式設計: 參數傳遞、回傳值、作用域
  2. 控制流程: if/match 表達式、迴圈控制
  3. 錯誤處理: Option 和 Result 型別的基礎應用
  4. 迭代器: 函數式程式設計風格
  5. 專案組織: 模組化設計和程式碼結構

實務技能

  • 終端機程式開發
  • 使用者互動設計
  • 資料結構操作
  • 錯誤處理策略
  • 程式碼測試基礎

下個章節預告

我們將開始導入所有權系統,Stay tuned!


上一篇
Day 6: 迴圈進階與迭代器
下一篇
Day 8: 理解 Rust 所有權系統與記憶體管理 (更新)
系列文
30天Rust從零到全端15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言