iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0
Rust

Rust 30 天養成計畫:從零到 CLI 專案系列 第 30

Day 30:小專案 — 可互動任務清單

  • 分享至 

  • xImage
  •  

專案主題
製作一個可以在命令列互動操作的「任務清單(Todo List)」系統,讓使用者能夠新增任務、標記完成或顯示目前清單。

1. 規劃
我希望讓多個使用者都能操作同一份任務清單:

  • 使用 RefCell 讓任務清單能在執行時修改內容。
  • 使用 Rc 讓整個清單的擁有權可安全地在多處引用(預留擴充空間)。
  • 提供命令列互動:
    (1)add <任務名稱> ➜ 新增任務
    (2)done <任務名稱> ➜ 標記完成
    (3)show ➜ 顯示清單
    (4)exit ➜ 結束程式

2. 建立專案

cargo new todo_rust
cd todo_rust

3. 撰寫主程式(非最終版本)

use std::cell::RefCell;
use std::io::{self, Write};
use std::rc::Rc;

#[derive(Debug)]
struct Task {
    name: String,
    done: bool,
}

#[derive(Debug)]
struct TodoList {
    tasks: Vec<Task>,
}

impl TodoList {
    fn new() -> Self {
        TodoList { tasks: Vec::new() }
    }

    fn add_task(&mut self, name: &str) {
        self.tasks.push(Task {
            name: name.to_string(),
            done: false,
        });
        println!("🆕 已新增任務:{}", name);
    }

    fn complete_task(&mut self, name: &str) {
        for task in &mut self.tasks {
            if task.name == name {
                task.done = true;
                println!("✅ 任務 '{}' 已完成!", name);
                return;
            }
        }
        println!("⚠️ 找不到任務 '{}'", name);
    }

    fn show(&self) {
        println!("\n📋 目前任務清單:");
        if self.tasks.is_empty() {
            println!("(目前沒有任務)");
        } else {
            for t in &self.tasks {
                let status = if t.done { "✔ 已完成" } else { "⏳ 未完成" };
                println!("- {:<20} {}", t.name, status);
            }
        }
    }
}

fn main() {
    let todo = Rc::new(RefCell::new(TodoList::new()));

    println!("=== 🧭 可互動任務清單系統 ===");
    println!("指令說明:");
    println!(" add <任務名稱>   ➜ 新增任務");
    println!(" done <任務名稱>  ➜ 標記完成");
    println!(" show             ➜ 顯示清單");
    println!(" exit             ➜ 結束程式");
    println!("============================");

    loop {
        print!("> ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("讀取輸入失敗");
        let input = input.trim();

        if input == "exit" {
            println!("👋 離開系統,再見!");
            break;
        } else if input.starts_with("add ") {
            let name = input.strip_prefix("add ").unwrap();
            todo.borrow_mut().add_task(name);
        } else if input.starts_with("done ") {
            let name = input.strip_prefix("done ").unwrap();
            todo.borrow_mut().complete_task(name);
        } else if input == "show" {
            todo.borrow().show();
        } else {
            println!("❓ 無效指令,請輸入 add / done / show / exit");
        }
    }

    println!("\n📊 Rc 強引用數量:{}", Rc::strong_count(&todo));
}

4. 測試與除錯過程
在測試過程中我發現我沒有注意到現在的程式碼是在記憶體內運行,所以當程式結束時,之前輸入過的內容都會消失,如下圖所示,再次運行程式時之前輸入過的內容會消失。
https://ithelp.ithome.com.tw/upload/images/20251014/20178873Xeq0Qhgr37.png
發現這個問題時我決定新增一個文字檔儲存加入過的內容,透過檔案讀寫(File I/O)將 Rc<RefCell> 中的清單內容序列化並寫入文字檔。
這個改動是建立在原本的程式之上,主要有幾個改動:

  • 程式啟動時自動從 tasks.txt 讀取任務資料。
  • 每次 add 或 done 後自動更新檔案。
  • 若檔案不存在,則自動建立新的空清單。
    一開始我在開發檔案儲存功能時,曾寫下:
use std::fs::{self, File};

原本的想法是預留 File::create() 的用法(以防未來要逐行寫入),但最終實作改用 fs::write() 和 fs::read_to_string(),所以 File 並沒有實際被用到,導致編譯時出現警告:

warning: unused import: `File`

為了保持乾淨的輸出,我將該行修改為:

use std::fs;

或者也可以用指令自動移除未使用的匯入:

cargo fix

5. 最終成果展示
程式碼:

use std::cell::RefCell;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
use std::rc::Rc;

#[derive(Debug)]
struct Task {
    name: String,
    done: bool,
}

#[derive(Debug)]
struct TodoList {
    tasks: Vec<Task>,
}

impl TodoList {
    fn new() -> Self {
        TodoList { tasks: Vec::new() }
    }

    fn load_from_file(filename: &str) -> Self {
        if Path::new(filename).exists() {
            let data = fs::read_to_string(filename).unwrap_or_default();
            let mut list = TodoList::new();
            for line in data.lines() {
                if let Some((name, status)) = line.split_once('|') {
                    list.tasks.push(Task {
                        name: name.to_string(),
                        done: status == "done",
                    });
                }
            }
            list
        } else {
            TodoList::new()
        }
    }

    fn save_to_file(&self, filename: &str) {
        let mut content = String::new();
        for t in &self.tasks {
            let status = if t.done { "done" } else { "todo" };
            content.push_str(&format!("{}|{}\n", t.name, status));
        }
        fs::write(filename, content).expect("無法寫入檔案");
    }

    fn add_task(&mut self, name: &str, filename: &str) {
        self.tasks.push(Task {
            name: name.to_string(),
            done: false,
        });
        println!("🆕 已新增任務:{}", name);
        self.save_to_file(filename);
    }

    fn complete_task(&mut self, name: &str, filename: &str) {
        for task in &mut self.tasks {
            if task.name == name {
                task.done = true;
                println!("✅ 任務 '{}' 已完成!", name);
                self.save_to_file(filename);
                return;
            }
        }
        println!("⚠️ 找不到任務 '{}'", name);
    }

    fn show(&self) {
        println!("\n📋 目前任務清單:");
        if self.tasks.is_empty() {
            println!("(目前沒有任務)");
        } else {
            for t in &self.tasks {
                let status = if t.done { "✔ 已完成" } else { "⏳ 未完成" };
                println!("- {:<20} {}", t.name, status);
            }
        }
    }
}

fn main() {
    let filename = "tasks.txt";
    let todo = Rc::new(RefCell::new(TodoList::load_from_file(filename)));

    println!("=== 🧭 可互動任務清單(自動儲存版) ===");
    println!("指令說明:");
    println!(" add <任務名稱>   ➜ 新增任務");
    println!(" done <任務名稱>  ➜ 標記完成");
    println!(" show             ➜ 顯示清單");
    println!(" exit             ➜ 結束程式");
    println!("==================================");

    loop {
        print!("> ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("讀取輸入失敗");
        let input = input.trim();

        if input == "exit" {
            println!("👋 離開系統,再見!");
            break;
        } else if input.starts_with("add ") {
            let name = input.strip_prefix("add ").unwrap();
            todo.borrow_mut().add_task(name, filename);
        } else if input.starts_with("done ") {
            let name = input.strip_prefix("done ").unwrap();
            todo.borrow_mut().complete_task(name, filename);
        } else if input == "show" {
            todo.borrow().show();
        } else {
            println!("❓ 無效指令,請輸入 add / done / show / exit");
        }
    }

    println!("\n📊 Rc 強引用數量:{}", Rc::strong_count(&todo));
}

輸入:

cargo run

執行結果:
https://ithelp.ithome.com.tw/upload/images/20251014/20178873flOubl64YE.png
直接打show會顯示之前輸入過的內容,之前編譯時出現的警告也消失。

6. 學習心得與補充
今天的專案為整個 30 天學習劃下句點,這次我不僅結合了智慧指標 Rc 與 RefCell,還學會讓資料能在實際系統中保存。過程中從警告訊息到實際除錯,每一步都讓我更理解 Rust 編譯器的嚴謹與友善。Rust 不只是語法安全,它讓我習慣主動思考為什麼這行程式存在,相信這個習慣能讓我使用其他程式語言時也受益。最後,看到清單能跨次啟動保存資料、警告消失、輸出乾淨的那一刻,我真的有種從學習走向實作的成就感。

7. 30天學習總結
經過這三十天的學習,我從最初的基礎語法開始,逐步掌握了 Rust 的核心觀念,包括所有權、借用、生命週期、泛型、trait、錯誤處理與智慧指標。這些主題在一開始確實不容易理解,但隨著實際練習與專案實作,我開始看出它們之間的關聯,也能理解 Rust 為什麼要這樣設計。特別是在後半段的小專案中,當我將 Rc、RefCell、Result 和檔案操作結合起來時,我發現自己已能完成一個結構清楚、邏輯安全的應用。這段過程讓我更熟悉 Rust 的思維方式,也培養出更注重邏輯與安全的寫程式習慣。整體來說,這三十天不只是語法練習,而是一次從理解理論到能實際應用的轉變,讓我更有信心繼續往進階主題與大型專案邁進。


上一篇
Day 29:RefCell<T> 與內部可變性(Interior Mutability)
系列文
Rust 30 天養成計畫:從零到 CLI 專案30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言