Dự án: danh sách việc cần làm (CLI) bằng Rust
Trong dự án này, bạn sẽ xây một danh sách việc cần làm nhỏ sống trong terminal. Bạn gõ todo add Mua sữa, và nó ghi nhớ. Đóng terminal, mai quay lại, gõ todo list, và các công việc vẫn còn đó.
Đây là một dự án thứ hai tuyệt vời vì nó trông như một công cụ thật. Nó dùng struct, enum, vector, đối số dòng lệnh, đọc ghi file và Result với ?. Ta sẽ xây từng mảnh riêng, thử nó trên Playground, rồi ghép tất cả lại.
Bạn sẽ xây dựng gì
Phần tiêu đề “Bạn sẽ xây dựng gì”Đây là cảm giác khi dùng công cụ hoàn chỉnh:
$ cargo run -- add Mua sữaĐã thêm công việc 1.$ cargo run -- add Gọi cho bàĐã thêm công việc 2.$ cargo run -- done 1Đã đánh dấu công việc 1 là xong.$ cargo run -- list[x] 1: Mua sữa[ ] 2: Gọi cho bà$ cargo run -- remove 1Đã xóa công việc 1: Mua sữaMọi thứ sau -- được truyền cho chương trình của bạn, không phải cho Cargo. Các công việc được lưu trong file tasks.txt, mỗi dòng một công việc:
2|0|Gọi cho bàMỗi dòng có dạng id|done|text, trong đó done là 1 nếu đã xong và 0 nếu chưa xong. Các lệnh (add, list, done, remove) giữ nguyên tiếng Anh, giống như phần lớn công cụ dòng lệnh.
Bắt đầu một project mới bằng:
cargo new todocd todoBước 1: Struct Task
Phần tiêu đề “Bước 1: Struct Task”Mỗi công việc có một số thứ tự (để bạn nhắc đến nó), một cờ đã xong, và một ít chữ. Một struct là lựa chọn tự nhiên. Ta cũng cho nó hai phương thức: một để biến công việc thành một dòng chữ cho file, và một để đọc công việc ngược lại từ một dòng.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { // Biến một công việc thành một dòng, ví dụ "1|0|Mua sữa". fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
// Đọc lại một công việc từ một dòng. Trả về None nếu dòng bị hỏng. fn from_line(line: &str) -> Option<Task> { let mut parts = line.splitn(3, '|'); let id = parts.next()?.parse().ok()?; let done = parts.next()? == "1"; let text = parts.next()?.to_string(); Some(Task { id, done, text }) }}
fn main() { let task = Task { id: 1, done: false, text: String::from("Mua sữa") }; let line = task.to_line(); println!("{line}");
println!("{:?}", Task::from_line("2|1|Gọi cho bà")); println!("{:?}", Task::from_line("3|0|Tìm hiểu về A|B testing")); println!("{:?}", Task::from_line("dòng này bị hỏng"));}Kết quả:
1|0|Mua sữaSome(Task { id: 2, done: true, text: "Gọi cho bà" })Some(Task { id: 3, done: false, text: "Tìm hiểu về A|B testing" })NoneCó hai chi tiết đáng dừng lại:
splitn(3, '|')tách thành tối đa 3 mảnh. Hai ký tự|đầu tiên phân cách các trường, và mọi thứ sau đó là nội dung, kể cả khi nội dung tự chứa một dấu|. Vớisplit('|')thường, công việc “A|B testing” sẽ mất một nửa nội dung.from_linetrả về mộtOption, và dùng?ở mỗi bước. Nếu thiếu mảnh nào hoặc id không phải là số, cả hàm trả vềNone..ok()biếnResulttừparse()thành mộtOptionđể?dùng được trên nó.
Bước 2: Enum Command
Phần tiêu đề “Bước 2: Enum Command”Công cụ hiểu bốn lệnh. Mỗi lệnh là một biến thể của một enum, và một số biến thể mang theo dữ liệu: Add cần nội dung, Done và Remove cần một số thứ tự công việc.
#[derive(Debug)]enum Command { Add(String), List, Done(u32), Remove(u32),}
fn parse_id(arg: Option<&String>) -> Result<u32, String> { let arg = arg.ok_or(String::from("hãy cho biết số thứ tự công việc"))?; arg.parse().map_err(|_| format!("'{arg}' không phải là số thứ tự công việc"))}
fn parse_command(args: &[String]) -> Result<Command, String> { match args.first().map(|s| s.as_str()) { None | Some("list") => Ok(Command::List), Some("add") => { let text = args[1..].join(" "); if text.is_empty() { Err(String::from("cách dùng: todo add <nội dung công việc>")) } else { Ok(Command::Add(text)) } } Some("done") => parse_id(args.get(1)).map(Command::Done), Some("remove") => parse_id(args.get(1)).map(Command::Remove), Some(other) => Err(format!("lệnh không xác định '{other}'")), }}
fn main() { let examples = [ vec!["add", "Mua", "sữa"], vec!["list"], vec![], vec!["done", "2"], vec!["done", "two"], vec!["remove"], vec!["dance"], ];
for words in examples { let args: Vec<String> = words.iter().map(|w| w.to_string()).collect(); println!("{:?} -> {:?}", words, parse_command(&args)); }}Kết quả:
["add", "Mua", "sữa"] -> Ok(Add("Mua sữa"))["list"] -> Ok(List)[] -> Ok(List)["done", "2"] -> Ok(Done(2))["done", "two"] -> Err("'two' không phải là số thứ tự công việc")["remove"] -> Err("hãy cho biết số thứ tự công việc")["dance"] -> Err("lệnh không xác định 'dance'")Trong chương trình thật, args đến từ dòng lệnh. Như bạn đã thấy ở trang Đối số dòng lệnh, phần tử đầu tiên từ std::env::args() là tên của chính chương trình, nên ta bỏ qua nó:
let args: Vec<String> = std::env::args().skip(1).collect();Vì sao add dùng args[1..].join(" ")? Shell của bạn tách todo add Mua sữa thành các từ riêng: add, Mua, sữa. Nối mọi thứ sau add lại sẽ ghép nội dung công việc như cũ, nên bạn không cần dấu nháy.
Để ý rằng parse_command không bao giờ crash. Dữ liệu nhập sai trở thành một Err với thông báo hữu ích. Nơi gọi hàm sẽ quyết định làm gì với nó.
Bước 3: Làm việc với danh sách công việc
Phần tiêu đề “Bước 3: Làm việc với danh sách công việc”Mọi công việc nằm trong một Vec<Task>. Mỗi lệnh là một hàm nhỏ thay đổi vector:
- Add: tìm id lớn nhất hiện có, cộng 1, rồi push một công việc mới.
- Done: tìm công việc có id đó và đặt
done = true. - Remove: tìm vị trí của công việc và xóa nó.
- List: in mọi công việc.
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
fn add_task(tasks: &mut Vec<Task>, text: String) -> u32 { let id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; tasks.push(Task { id, done: false, text }); id}
fn mark_done(tasks: &mut [Task], id: u32) -> Result<(), String> { match tasks.iter_mut().find(|t| t.id == id) { Some(task) => { task.done = true; Ok(()) } None => Err(format!("không có công việc {id}")), }}
fn remove_task(tasks: &mut Vec<Task>, id: u32) -> Result<Task, String> { match tasks.iter().position(|t| t.id == id) { Some(index) => Ok(tasks.remove(index)), None => Err(format!("không có công việc {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Không có việc gì cả. Chúc bạn một ngày vui vẻ!"); return; } for task in tasks { let mark = if task.done { "x" } else { " " }; println!("[{mark}] {}: {}", task.id, task.text); }}
fn main() { let mut tasks = Vec::new(); add_task(&mut tasks, String::from("Mua sữa")); add_task(&mut tasks, String::from("Gọi cho bà")); add_task(&mut tasks, String::from("Học Rust"));
mark_done(&mut tasks, 3).unwrap(); let removed = remove_task(&mut tasks, 1).unwrap(); println!("Đã xóa: {}", removed.text);
if let Err(message) = mark_done(&mut tasks, 99) { println!("Lỗi: {message}"); }
print_tasks(&tasks);}Kết quả:
Đã xóa: Mua sữaLỗi: không có công việc 99[ ] 2: Gọi cho bà[x] 3: Học RustVài lựa chọn ở đây là có chủ ý:
- Id lấy từ id lớn nhất hiện có, không phải từ
tasks.len() + 1. Nếu bạn xóa công việc 1 rồi thêm một công việc mới, dùng độ dài sẽ cho ra một id trùng. mark_donenhận&mut [Task](một slice) vì nó chỉ thay đổi các phần tử.add_taskvàremove_tasknhận&mut Vec<Task>vì chúng thay đổi độ dài.- Lỗi là
Result<_, String>. Lỗi kiểuStringlà thứ đơn giản nhất mà vẫn chạy tốt. Với chương trình lớn hơn, xem Lỗi tùy chỉnh.
iter_mut().find(...) cho bạn một tham chiếu khả biến tới công việc khớp, đó là lý do task.done = true thay đổi được công việc nằm trong vector. Xem Iterator để biết thêm về find và position.
Bước 4: Lưu và nạp file
Phần tiêu đề “Bước 4: Lưu và nạp file”Giờ các công việc cần sống sót sau khi chương trình kết thúc. Ta lưu chúng bằng std::fs::write và nạp chúng bằng std::fs::read_to_string. Cả hai đều có thể thất bại, nên cả hai trả về Result, và toán tử ? chuyển mọi lỗi lên cho nơi gọi:
use std::fs;use std::io;
fn save_tasks(path: &str, tasks: &[Task]) -> Result<(), io::Error> { let mut text = String::new(); for task in tasks { text.push_str(&task.to_line()); text.push('\n'); } fs::write(path, text)?; Ok(())}
fn load_tasks(path: &str) -> Result<Vec<Task>, io::Error> { let text = match fs::read_to_string(path) { Ok(text) => text, // Lần đầu bạn chạy chương trình, chưa có file nào. // Đó không phải lỗi: nó chỉ có nghĩa là "chưa có công việc nào". Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), Err(e) => return Err(e), }; let tasks = text.lines().filter_map(Task::from_line).collect(); Ok(tasks)}Trường hợp NotFound quan trọng hơn vẻ ngoài của nó. Thiếu nó, lệnh todo add đầu tiên sẽ thất bại với “No such file or directory” (không có file hoặc thư mục đó), vì bạn chưa lưu gì cả. Mọi lỗi khác (như “permission denied”, không có quyền truy cập) vẫn được chuyển lên.
filter_map(Task::from_line) đọc từng dòng và chỉ giữ những dòng parse được. Hãy lưu ý rằng nó lặng lẽ bỏ qua các dòng hỏng. Điều đó thân thiện nếu ai đó sửa file bằng tay và gõ nhầm, nhưng cũng có nghĩa là một lỗi có thể trôi qua mà không ai để ý. Một ý tưởng mở rộng ở bên dưới là cảnh báo về chúng.
Xem Đọc và ghi file để biết thêm về đọc và ghi file.
Bước 5: Chương trình hoàn chỉnh
Phần tiêu đề “Bước 5: Chương trình hoàn chỉnh”Đây là mọi thứ ghép lại. Đặt nó vào src/main.rs và chạy bằng cargo run -- add Mua sữa, cargo run -- list, và cứ thế.
Logic cốt lõi nằm trong run_command, hàm làm việc trên một Vec<Task> và không biết gì về file. main chỉ nạp file, gọi run_command, rồi lưu lại. Tách chúng ra giúp logic dễ test, và đó là điều cho phép ta làm một phiên bản Playground ở bước tiếp theo.
use std::env;use std::error::Error;use std::fs;use std::io;use std::process;
const FILE: &str = "tasks.txt";
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }
fn from_line(line: &str) -> Option<Task> { let mut parts = line.splitn(3, '|'); let id = parts.next()?.parse().ok()?; let done = parts.next()? == "1"; let text = parts.next()?.to_string(); Some(Task { id, done, text }) }}
#[derive(Debug)]enum Command { Add(String), List, Done(u32), Remove(u32),}
fn parse_id(arg: Option<&String>) -> Result<u32, String> { let arg = arg.ok_or(String::from("hãy cho biết số thứ tự công việc"))?; arg.parse().map_err(|_| format!("'{arg}' không phải là số thứ tự công việc"))}
fn parse_command(args: &[String]) -> Result<Command, String> { match args.first().map(|s| s.as_str()) { None | Some("list") => Ok(Command::List), Some("add") => { let text = args[1..].join(" "); if text.is_empty() { Err(String::from("cách dùng: todo add <nội dung công việc>")) } else { Ok(Command::Add(text)) } } Some("done") => parse_id(args.get(1)).map(Command::Done), Some("remove") => parse_id(args.get(1)).map(Command::Remove), Some(other) => Err(format!("lệnh không xác định '{other}'")), }}
fn add_task(tasks: &mut Vec<Task>, text: String) -> u32 { let id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; tasks.push(Task { id, done: false, text }); id}
fn mark_done(tasks: &mut [Task], id: u32) -> Result<(), String> { match tasks.iter_mut().find(|t| t.id == id) { Some(task) => { task.done = true; Ok(()) } None => Err(format!("không có công việc {id}")), }}
fn remove_task(tasks: &mut Vec<Task>, id: u32) -> Result<Task, String> { match tasks.iter().position(|t| t.id == id) { Some(index) => Ok(tasks.remove(index)), None => Err(format!("không có công việc {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Không có việc gì cả. Chúc bạn một ngày vui vẻ!"); return; } for task in tasks { let mark = if task.done { "x" } else { " " }; println!("[{mark}] {}: {}", task.id, task.text); }}
fn run_command(args: &[String], tasks: &mut Vec<Task>) -> Result<(), String> { match parse_command(args)? { Command::Add(text) => { let id = add_task(tasks, text); println!("Đã thêm công việc {id}."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Đã đánh dấu công việc {id} là xong."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Đã xóa công việc {id}: {}", task.text); } } Ok(())}
fn save_tasks(path: &str, tasks: &[Task]) -> Result<(), io::Error> { let mut text = String::new(); for task in tasks { text.push_str(&task.to_line()); text.push('\n'); } fs::write(path, text)?; Ok(())}
fn load_tasks(path: &str) -> Result<Vec<Task>, io::Error> { let text = match fs::read_to_string(path) { Ok(text) => text, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), Err(e) => return Err(e), }; let tasks = text.lines().filter_map(Task::from_line).collect(); Ok(tasks)}
fn run() -> Result<(), Box<dyn Error>> { let args: Vec<String> = env::args().skip(1).collect(); let mut tasks = load_tasks(FILE)?; run_command(&args, &mut tasks)?; save_tasks(FILE, &tasks)?; Ok(())}
fn main() { if let Err(e) = run() { eprintln!("Lỗi: {e}"); process::exit(1); }}Một lưu ý về run() -> Result<(), Box<dyn Error>>. Các hàm của ta trả về hai kiểu lỗi khác nhau: io::Error từ các hàm file và String từ các hàm xử lý lệnh. Box<dyn Error> là một chiếc hộp chứa được bất kỳ kiểu lỗi nào, nên ? dùng được với cả hai. Sau đó main in lỗi bằng eprintln! (ghi ra luồng lỗi) và thoát với mã 1, tín hiệu thông thường báo rằng có gì đó không ổn.
Bước 6: Phiên bản chạy được trên Playground
Phần tiêu đề “Bước 6: Phiên bản chạy được trên Playground”Playground không truyền được đối số dòng lệnh, và ta không muốn nó phụ thuộc vào file. Vì vậy phiên bản này giữ nguyên run_command, nhưng cho nó chạy một danh sách lệnh soạn sẵn và in ra những gì tasks.txt sẽ chứa ở cuối:
#[derive(Debug)]struct Task { id: u32, done: bool, text: String,}
impl Task { fn to_line(&self) -> String { let done = if self.done { 1 } else { 0 }; format!("{}|{}|{}", self.id, done, self.text) }}
#[derive(Debug)]enum Command { Add(String), List, Done(u32), Remove(u32),}
fn parse_id(arg: Option<&String>) -> Result<u32, String> { let arg = arg.ok_or(String::from("hãy cho biết số thứ tự công việc"))?; arg.parse().map_err(|_| format!("'{arg}' không phải là số thứ tự công việc"))}
fn parse_command(args: &[String]) -> Result<Command, String> { match args.first().map(|s| s.as_str()) { None | Some("list") => Ok(Command::List), Some("add") => { let text = args[1..].join(" "); if text.is_empty() { Err(String::from("cách dùng: todo add <nội dung công việc>")) } else { Ok(Command::Add(text)) } } Some("done") => parse_id(args.get(1)).map(Command::Done), Some("remove") => parse_id(args.get(1)).map(Command::Remove), Some(other) => Err(format!("lệnh không xác định '{other}'")), }}
fn add_task(tasks: &mut Vec<Task>, text: String) -> u32 { let id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; tasks.push(Task { id, done: false, text }); id}
fn mark_done(tasks: &mut [Task], id: u32) -> Result<(), String> { match tasks.iter_mut().find(|t| t.id == id) { Some(task) => { task.done = true; Ok(()) } None => Err(format!("không có công việc {id}")), }}
fn remove_task(tasks: &mut Vec<Task>, id: u32) -> Result<Task, String> { match tasks.iter().position(|t| t.id == id) { Some(index) => Ok(tasks.remove(index)), None => Err(format!("không có công việc {id}")), }}
fn print_tasks(tasks: &[Task]) { if tasks.is_empty() { println!("Không có việc gì cả. Chúc bạn một ngày vui vẻ!"); return; } for task in tasks { let mark = if task.done { "x" } else { " " }; println!("[{mark}] {}: {}", task.id, task.text); }}
fn run_command(args: &[String], tasks: &mut Vec<Task>) -> Result<(), String> { match parse_command(args)? { Command::Add(text) => { let id = add_task(tasks, text); println!("Đã thêm công việc {id}."); } Command::List => print_tasks(tasks), Command::Done(id) => { mark_done(tasks, id)?; println!("Đã đánh dấu công việc {id} là xong."); } Command::Remove(id) => { let task = remove_task(tasks, id)?; println!("Đã xóa công việc {id}: {}", task.text); } } Ok(())}
fn main() { // Mỗi dòng là thứ bạn sẽ gõ sau `todo`. let script = [ "list", "add Mua sữa", "add Gọi cho bà", "add Học Rust", "done 3", "remove 1", "done 9", "jump", "list", ];
let mut tasks: Vec<Task> = Vec::new();
for line in script { println!("$ todo {line}"); let args: Vec<String> = line.split_whitespace().map(String::from).collect(); if let Err(message) = run_command(&args, &mut tasks) { println!("Lỗi: {message}"); } }
println!(); println!("tasks.txt sẽ chứa:"); for task in &tasks { println!("{}", task.to_line()); }}Kết quả:
$ todo listKhông có việc gì cả. Chúc bạn một ngày vui vẻ!$ todo add Mua sữaĐã thêm công việc 1.$ todo add Gọi cho bàĐã thêm công việc 2.$ todo add Học RustĐã thêm công việc 3.$ todo done 3Đã đánh dấu công việc 3 là xong.$ todo remove 1Đã xóa công việc 1: Mua sữa$ todo done 9Lỗi: không có công việc 9$ todo jumpLỗi: lệnh không xác định 'jump'$ todo list[ ] 2: Gọi cho bà[x] 3: Học Rust
tasks.txt sẽ chứa:2|0|Gọi cho bà3|1|Học RustHãy đổi kịch bản và chạy lại. Thử thêm một công việc sau khi xóa một công việc khác và kiểm tra rằng id mới không bị trùng.
Nâng cấp: lưu thành JSON với serde
Phần tiêu đề “Nâng cấp: lưu thành JSON với serde”Định dạng id|done|text của ta dễ đọc, nhưng bạn phải tự tay viết to_line và from_line. Khi công việc có thêm thông tin (hạn chót, độ ưu tiên, nhãn), phần code viết tay đó cũng phình ra theo.
Trong các project thật, người ta thường để các crate serde (tiếng Anh) làm việc này. Thêm chúng bằng:
cargo add serde --features derivecargo add serde_jsonSau đó chỉ một dòng derive là bạn có ngay việc lưu và nạp:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]struct Task { id: u32, done: bool, text: String,}
fn main() { let tasks = vec![ Task { id: 1, done: false, text: String::from("Mua sữa") }, Task { id: 2, done: true, text: String::from("Gọi cho bà") }, ];
let json = serde_json::to_string(&tasks).unwrap(); println!("{json}");
let loaded: Vec<Task> = serde_json::from_str(&json).unwrap(); println!("Đã nạp lại {} công việc", loaded.len());}Kết quả:
[{"id":1,"done":false,"text":"Mua sữa"},{"id":2,"done":true,"text":"Gọi cho bà"}]Đã nạp lại 2 công việcBạn sẽ ghi chuỗi JSON đó vào tasks.json bằng fs::write, y như trước. Trang JSON với serde nói chi tiết về việc này.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Dùng ? trong một main không trả về gì. Rất dễ bị cám dỗ viết thế này:
use std::fs;
fn main() { let text = fs::read_to_string("tasks.txt")?; println!("{text}");}error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:47 |3 | fn main() { | --------- this function should return `Result` or `Option` to accept `?`4 | let text = fs::read_to_string("tasks.txt")?; | ^ cannot use the `?` operator in a function that returns `()`Trình biên dịch báo rằng toán tử ? chỉ dùng được trong hàm trả về Result hoặc Option. ? nghĩa là “nếu đây là lỗi, hãy trả nó về từ hàm hiện tại”. Một main bình thường trả về (), nên không có chỗ nào để gửi lỗi đi. Hoặc bạn cho main một kiểu trả về như Result<(), Box<dyn Error>>, hoặc làm như chương trình của ta: đặt công việc vào một hàm run() trả về Result, và xử lý lỗi một lần trong main.
Coi file không tồn tại là thất bại. Nếu load_tasks chỉ làm fs::read_to_string(path)?, chương trình sẽ thất bại ngay lần chạy đầu tiên, trước khi có file nào. Hãy kiểm tra io::ErrorKind::NotFound và bắt đầu với một danh sách rỗng.
Dùng độ dài danh sách làm id tiếp theo. Với các công việc 1, 2, 3, xóa công việc 1 sẽ còn lại hai công việc. len() + 1 lại cho ra 3, và giờ hai công việc dùng chung một id. Hãy dùng id lớn nhất cộng một.
Ý tưởng để mở rộng
Phần tiêu đề “Ý tưởng để mở rộng”- Hủy “đã xong”. Thêm lệnh
undone <id>đặtdonetrở lạifalse. - Sửa một công việc.
edit 2 Gọi cho bà tối naythay nội dung của công việc 2. - Dọn các công việc đã xong.
clearxóa mọi công việc đã xong. Hãy xemVec::retain(tiếng Anh). - Cảnh báo về dòng hỏng. Thay vì lặng lẽ bỏ qua dòng sai, hãy in một cảnh báo kèm số dòng.
- Một lệnh
helpin ra mọi lệnh và cách dùng chúng. - Viết test.
parse_commandvàrun_commandkhông đụng đến file, nên rất dễ test. - Dùng một bộ phân tích đối số thật. Crate
clap(tiếng Anh), được nhắc đến ở trang Đối số dòng lệnh, cho bạn--helpvà lệnh con (subcommand) miễn phí.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Một struct
Taskchứa dữ liệu.to_linevàfrom_linebiến nó thành chữ và ngược lại. - Một enum
Commandmô tả điều người dùng yêu cầu.parse_commandbiến đối số thành mộtCommand, hoặc mộtErrvới thông báo hữu ích. - Một
Vec<Task>chứa danh sách. Các hàm nhỏ thêm, tìm, thay đổi và xóa công việc. fs::read_to_stringvàfs::writetrả vềResult, và?chuyển lỗi lên. File không tồn tại ở lần chạy đầu được xử lý như “chưa có công việc nào”.- Tách logic (
run_command) khỏi file và đối số giúp nó dễ test và chạy được trên Playground.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Vì sao from_line dùng splitn(3, '|') thay vì split('|')?
2. load_tasks làm gì khi tasks.txt chưa tồn tại?
3. Vì sao không dùng ? trực tiếp được bên trong fn main() không có kiểu trả về?
4. Crate nào là lựa chọn nâng cấp thông thường để lưu struct thành JSON?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- An I/O Project: Building a Command Line Program — The Rust Book (tiếng Anh): một dự án CLI có hướng dẫn lớn hơn, với lời khuyên hay về cách chia
mainthành những phần nhỏ hơn, dễ test hơn. std::fs— std docs (tiếng Anh):read_to_string,writevà các hàm file khác đằng sau phần code lưu và nạp.std::env::args— std docs (tiếng Anh): đối số dòng lệnh đến như thế nào, và vì sao đối số đầu tiên là tên của chính chương trình.str::splitn— std docs (tiếng Anh): tách một dòng thành tối đa N mảnh. Đó là mẹo giữ cho nội dung công việc có chứa|vẫn nguyên một mảnh.serde_json— docs.rs (tiếng Anh): crate JSON trong phần nâng cấp, với ví dụto_stringvàfrom_str. Hướng dẫn serde (tiếng Anh) giải thích các attribute của derive.