專案主題
製作一個可以在命令列互動操作的「任務清單(Todo List)」系統,讓使用者能夠新增任務、標記完成或顯示目前清單。
1. 規劃
我希望讓多個使用者都能操作同一份任務清單:
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. 測試與除錯過程
在測試過程中我發現我沒有注意到現在的程式碼是在記憶體內運行,所以當程式結束時,之前輸入過的內容都會消失,如下圖所示,再次運行程式時之前輸入過的內容會消失。
發現這個問題時我決定新增一個文字檔儲存加入過的內容,透過檔案讀寫(File I/O)將 Rc<RefCell> 中的清單內容序列化並寫入文字檔。
這個改動是建立在原本的程式之上,主要有幾個改動:
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
執行結果:
直接打show會顯示之前輸入過的內容,之前編譯時出現的警告也消失。
6. 學習心得與補充
今天的專案為整個 30 天學習劃下句點,這次我不僅結合了智慧指標 Rc 與 RefCell,還學會讓資料能在實際系統中保存。過程中從警告訊息到實際除錯,每一步都讓我更理解 Rust 編譯器的嚴謹與友善。Rust 不只是語法安全,它讓我習慣主動思考為什麼這行程式存在,相信這個習慣能讓我使用其他程式語言時也受益。最後,看到清單能跨次啟動保存資料、警告消失、輸出乾淨的那一刻,我真的有種從學習走向實作的成就感。
7. 30天學習總結
經過這三十天的學習,我從最初的基礎語法開始,逐步掌握了 Rust 的核心觀念,包括所有權、借用、生命週期、泛型、trait、錯誤處理與智慧指標。這些主題在一開始確實不容易理解,但隨著實際練習與專案實作,我開始看出它們之間的關聯,也能理解 Rust 為什麼要這樣設計。特別是在後半段的小專案中,當我將 Rc、RefCell、Result 和檔案操作結合起來時,我發現自己已能完成一個結構清楚、邏輯安全的應用。這段過程讓我更熟悉 Rust 的思維方式,也培養出更注重邏輯與安全的寫程式習慣。整體來說,這三十天不只是語法練習,而是一次從理解理論到能實際應用的轉變,讓我更有信心繼續往進階主題與大型專案邁進。