Rust 文件读写
读写文件是你学过的 Result 和 ? 运算符第一次真正派上用场的地方。几乎每个文件操作都可能失败(文件可能不存在,你可能没有权限),所以标准库会让你直接面对这些情况。
读取整个文件
Section titled “读取整个文件”把文件内容读进一个 String,最简单的写法是这样:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("无法读取文件:{e}"), }}fs::read_to_string 返回 Result<String, std::io::Error>:成功时是装着文本的 Ok,失败时是装着原因的 Err(文件不存在、没有权限等等)。
用 ? 简化错误处理
Section titled “用 ? 简化错误处理”每个文件操作都写一遍 match,很快就会变得重复。如果你所在的函数本身就返回 Result,? 运算符(见 Result 一页)能让代码清爽很多:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // 失败时提前返回 Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("出错了:{e}"), }}use std::fs;
fn main() { let result = fs::write("output.txt", "你好,文件!");
match result { Ok(()) => println!("写入成功"), Err(e) => println!("写入失败:{e}"), }}fs::write 在文件不存在时会创建它,在文件已存在时会把它整个覆盖掉。如果你想往文件里追加内容而不是替换它,要记住这一点。
追加到文件末尾
Section titled “追加到文件末尾”想往已有的文件里添加内容而不是覆盖它,就用 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, "新的一行日志")?; Ok(())}注意这里的 main 本身返回 Result。这是一个值得记住的写法:当 main 返回 Result 时,你可以直接在里面用 ?,不用把所有东西都包进 match。
对于很大的文件,一次把整个文件读进内存并不总是好主意。改成逐行读取:
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(())}判断发生了哪种错误
Section titled “判断发生了哪种错误”到目前为止,所有错误都是同样处理的:打印出来。但很多时候,你会想根据出了什么问题做出不同的反应。文件不存在通常没关系(也许是第一次运行,从空白开始就行);而“权限被拒绝”就是需要报告的真问题了。
io::Error 可以用 .kind() 告诉你它属于哪一类,返回的是一个 ErrorKind(英文)枚举,可以对它 match:
use std::fs;use std::io::ErrorKind;
fn main() { // 系统临时文件夹里一个不存在的文件 let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("已加载 {} 行", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("还没有笔记,从头开始"), ErrorKind::PermissionDenied => println!("没有权限读取笔记文件"), _ => println!("出了别的问题:{e}"), }, }}输出:
还没有笔记,从头开始std::env::temp_dir() 给出系统临时文件夹的路径(比如 Linux 上的 /tmp),.join(...) 再往后面拼上一个文件名。文件相关的函数接受这样的路径,和接受 &str 一样顺手。
最后一定要留一个 _ 分支:ErrorKind 的变体很多,而且还会不断增加。_ 分支能接住所有你没列出来的情况。
两道练习都会先在临时文件夹里写好自己的文件,所以在任何地方都能运行,包括 Playground。
1. 保存并数行数
Section titled “1. 保存并数行数”用 fs::write 把三条待办事项写进一个文件,每行一条。然后用 fs::read_to_string 把文件读回来,给每行加上编号打印出来,最后打印总数。
提示:在各项之间加上 \n 来分行。text.lines() 会一行一行地给你。让 main 返回 std::io::Result<()>,这样就能用 ?。
查看答案
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "学习所有权\n写一个模块\n读一个文件\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("共 {count} 项"); Ok(())}输出:
1. 学习所有权2. 写一个模块3. 读一个文件共 3 项因为 fs::write 会替换整个文件,所以无论运行多少次,你得到的都是同样的三行。写入或读取失败时,每个 ? 都会把错误从 main 返回出去。
2. 追加日志
Section titled “2. 追加日志”先写一行标题,让日志文件从头开始,然后用 OpenOptions 和 writeln! 再追加两行。最后把整个文件读回来打印出来。
提示:标题用 fs::write 写(这样每次运行都从干净的状态开始),追加的行用 OpenOptions::new().append(true).open(&path)?。文件本身已经以换行符结尾,所以用 print!(而不是 println!)可以避免末尾多出一个空行。
查看答案
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, "=== 日志 ===\n")?; // 从头开始
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "开始")?; writeln!(file, "结束")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}输出:
=== 日志 ===开始结束use std::fs::{self, OpenOptions}; 同时引入了 fs 模块本身和 OpenOptions。如果没有开头那次 fs::write,每次运行都会继续往旧文件里追加,日志就会越来越长。对真正的日志来说这正是你想要的,但对于输出固定的练习就不合适了。
fs::read_to_string(path)把整个文件读进一个String,返回Result。fs::write(path, contents)写入文件,如果文件已存在就覆盖它。OpenOptions::new().append(true)让你往文件里追加内容,而不是替换它。main() -> std::io::Result<()>让你可以直接在main里用?,不用对每次调用都写 match。e.kind()告诉你发生了哪种错误,比如ErrorKind::NotFound,这样就能分别处理。- 用到项目文件夹里文件的示例最好在本地试;
std::env::temp_dir()给你一个在哪儿都能用的文件夹。
小测验
1. 对一个已经存在的路径调用 fs::write,会发生什么?
2. 怎样往文件里追加内容,而不是覆盖它?
3. 让 main 本身返回 std::io::Result<()>,有什么好处?
4. fs::read_to_string(path)? 失败时,? 运算符会做什么?
5. 为什么读取大文件时要用 BufReader 逐行读取,而不是 fs::read_to_string?
得分: 0 / 5
std::fs— 标准库文档(英文):所有文件和文件夹相关的函数,比如create_dir_all、copy、rename和remove_file。- 读取文件 —《Rust 程序设计语言》:在官方教程的
minigrep项目里读取文件,把本页内容和命令行参数结合起来。 - 文件输入输出(I/O) — 通过例子学 Rust:打开、创建文件以及逐行读取文件的可运行示例。
OpenOptions— 标准库文档(英文):打开文件的各种方式(追加、创建、截断等等)。io::ErrorKind— 标准库文档(英文):可以用来 match 的全部错误类别。