Đọc ghi file trong Rust
Đọc và ghi file là một trong những chỗ đầu tiên mà mọi thứ bạn đã học về Result và toán tử ? thôi còn là lý thuyết — hầu như mọi thao tác với file đều có thể thất bại (file có thể không tồn tại, bạn có thể không có quyền), nên thư viện chuẩn buộc bạn phải xử lý chuyện đó một cách trực tiếp.
Đọc toàn bộ một file
Phần tiêu đề “Đọc toàn bộ một file”Cách đơn giản nhất để đọc nội dung một file vào một String:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("Couldn't read file: {e}"), }}fs::read_to_string trả về một Result<String, std::io::Error> — Ok kèm văn bản nếu thành công, Err kèm lý do nếu thất bại (thiếu file, không có quyền, v.v.).
Dùng ? để xử lý lỗi gọn hơn
Phần tiêu đề “Dùng ? để xử lý lỗi gọn hơn”match trên mọi thao tác file sẽ nhanh chóng trở nên lặp đi lặp lại. Nếu bạn đang ở trong một hàm cũng trả về Result, toán tử ? (trong bài Result) giúp code gọn đi rất nhiều:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // trả về sớm nếu thất bại Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Error: {e}"), }}Ghi vào file
Phần tiêu đề “Ghi vào file”use std::fs;
fn main() { let result = fs::write("output.txt", "Hello, file!");
match result { Ok(()) => println!("Wrote successfully"), Err(e) => println!("Failed to write: {e}"), }}fs::write tạo file nếu nó chưa tồn tại, và ghi đè hoàn toàn nếu nó đã có — hãy nhớ điều đó nếu bạn cần thêm vào file thay vì thay thế nó.
Ghi nối vào file
Phần tiêu đề “Ghi nối vào file”Để thêm vào một file đã có thay vì ghi đè nó, hãy dùng OpenOptions:
use std::fs::OpenOptions;use std::io::Write;
fn main() -> std::io::Result<()> { let mut file = OpenOptions::new() .append(true) .create(true) .open("log.txt")?;
writeln!(file, "A new log line")?; Ok(())}Để ý rằng ở đây chính main trả về Result — đó là một mẫu đáng biết: khi main trả về Result, bạn có thể dùng ? ngay bên trong nó thay vì bọc mọi thứ trong match.
Đọc từng dòng
Phần tiêu đề “Đọc từng dòng”Với file lớn, đọc toàn bộ vào bộ nhớ một lần không phải lúc nào cũng lý tưởng. Thay vào đó, hãy đọc từng dòng:
use std::fs::File;use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> { let file = File::open("hello.txt")?; let reader = BufReader::new(file);
for line in reader.lines() { println!("{}", line?); }
Ok(())}Kiểm tra lỗi nào đã xảy ra
Phần tiêu đề “Kiểm tra lỗi nào đã xảy ra”Đến giờ, mọi lỗi đều được xử lý theo cùng một cách: in ra. Nhưng thường bạn muốn phản ứng khác nhau tùy theo điều gì đã sai. Thiếu file thường không sao (có thể đây là lần chạy đầu tiên, và bạn chỉ cần bắt đầu với dữ liệu rỗng). “Permission denied” (không có quyền) thì là vấn đề thật mà bạn nên báo lại.
Một io::Error cho bạn biết loại lỗi của nó qua .kind(), phương thức này trả về một enum ErrorKind (tiếng Anh) mà bạn có thể match:
use std::fs;use std::io::ErrorKind;
fn main() { // một file không tồn tại trong thư mục tạm của hệ thống let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("Đã nạp {} dòng", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Chưa có ghi chú nào, bắt đầu từ đầu"), ErrorKind::PermissionDenied => println!("Không được phép đọc file ghi chú"), _ => println!("Có lỗi khác xảy ra: {e}"), }, }}Kết quả:
Chưa có ghi chú nào, bắt đầu từ đầustd::env::temp_dir() cho bạn đường dẫn của thư mục tạm của hệ thống (như /tmp trên Linux), và .join(...) thêm một tên file vào đó. Các hàm làm việc với file nhận đường dẫn kiểu này cũng vui vẻ như nhận một &str.
Luôn giữ một nhánh _ ở cuối: ErrorKind có rất nhiều biến thể, và theo thời gian còn được thêm mới. Nhánh _ bắt mọi thứ bạn chưa liệt kê.
Luyện tập
Phần tiêu đề “Luyện tập”Cả hai bài tập đều tự ghi file của mình vào thư mục tạm trước, nên chúng chạy được ở bất cứ đâu, kể cả Playground.
1. Lưu và đếm dòng
Phần tiêu đề “1. Lưu và đếm dòng”Ghi ba việc cần làm vào một file bằng fs::write, mỗi việc một dòng. Sau đó đọc lại file bằng fs::read_to_string và in từng dòng kèm số thứ tự phía trước, tiếp theo là tổng số việc.
Gợi ý: đặt \n giữa các việc để tách thành từng dòng. text.lines() cho bạn lần lượt từng dòng. Cho main trả về std::io::Result<()> để bạn dùng được ?.
Xem lời giải
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "học ownership\nviết một module\nđọc một file\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("{count} việc"); Ok(())}Kết quả:
1. học ownership2. viết một module3. đọc một file3 việcVì fs::write thay thế cả file, bạn luôn nhận được đúng ba dòng đó dù chạy bao nhiêu lần. Mỗi ? trả lỗi về từ main nếu việc ghi hoặc đọc thất bại.
2. Ghi nối vào nhật ký
Phần tiêu đề “2. Ghi nối vào nhật ký”Bắt đầu một file nhật ký mới với một dòng tiêu đề, rồi ghi nối thêm hai dòng vào đó bằng OpenOptions và writeln!. Cuối cùng, đọc lại toàn bộ file và in ra.
Gợi ý: dùng fs::write cho dòng tiêu đề (để mỗi lần chạy đều bắt đầu sạch sẽ), rồi OpenOptions::new().append(true).open(&path)? cho các dòng thêm vào. print! (không phải println!) tránh một dòng trống thừa ở cuối, vì file đã kết thúc bằng một ký tự xuống dòng.
Xem lời giải
use std::fs::{self, OpenOptions};use std::io::Write;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-log.txt");
fs::write(&path, "=== nhật ký ===\n")?; // bắt đầu mới
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "đã bắt đầu")?; writeln!(file, "đã kết thúc")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Kết quả:
=== nhật ký ===đã bắt đầuđã kết thúcuse std::fs::{self, OpenOptions}; đưa vào cả chính module fs lẫn OpenOptions. Nếu thiếu lệnh fs::write đầu tiên, mỗi lần chạy sẽ tiếp tục thêm vào file cũ, và nhật ký sẽ dài ra mãi. Đó đúng là điều bạn muốn với một nhật ký thật, nhưng không phải với một bài tập có kết quả cố định.
Tóm tắt
Phần tiêu đề “Tóm tắt”fs::read_to_string(path)đọc toàn bộ file vào mộtString, và trả về mộtResult.fs::write(path, contents)ghi một file, ghi đè nếu file đã tồn tại.OpenOptions::new().append(true)cho phép thêm vào file thay vì thay thế nó.main() -> std::io::Result<()>cho phép dùng?ngay trongmain, thay vìmatchmọi lời gọi.e.kind()cho bạn biết loại lỗi nào đã xảy ra, nhưErrorKind::NotFound, để bạn xử lý từng loại khác nhau.- Các ví dụ dùng file trong thư mục dự án nên được thử trên máy;
std::env::temp_dir()cho bạn một thư mục dùng được ở mọi nơi.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Điều gì xảy ra nếu bạn gọi fs::write với một đường dẫn đã tồn tại?
2. Làm sao để thêm vào file thay vì ghi đè nó?
3. Khi chính main trả về std::io::Result<()>, điều đó cho phép gì?
4. Toán tử ? làm gì khi fs::read_to_string(path)? thất bại?
5. Vì sao nên đọc file lớn từng dòng bằng BufReader thay vì fs::read_to_string?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”std::fs— tài liệu thư viện chuẩn (tiếng Anh): mọi hàm làm việc với file và thư mục, nhưcreate_dir_all,copy,renamevàremove_file.- Reading a File — The Rust Book (tiếng Anh): đọc file trong dự án
minigrepcủa Book, kết hợp nội dung bài này với đối số dòng lệnh. - File I/O — Rust by Example (tiếng Anh): ví dụ chạy được về mở, tạo và đọc file từng dòng.
OpenOptions— tài liệu thư viện chuẩn (tiếng Anh): mọi cách mở một file (append, create, truncate, v.v.).io::ErrorKind— tài liệu thư viện chuẩn (tiếng Anh): danh sách đầy đủ các loại lỗi mà bạn có thểmatch.