跳转到内容

Rust 文件读写

读写文件是你学过的 Result 和 ? 运算符第一次真正派上用场的地方。几乎每个文件操作都可能失败(文件可能不存在,你可能没有权限),所以标准库会让你直接面对这些情况。

把文件内容读进一个 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(文件不存在、没有权限等等)。

每个文件操作都写一遍 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 在文件不存在时会创建它,在文件已存在时会把它整个覆盖掉。如果你想往文件里追加内容而不是替换它,要记住这一点。

想往已有的文件里添加内容而不是覆盖它,就用 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(())
}

到目前为止,所有错误都是同样处理的:打印出来。但很多时候,你会想根据出了什么问题做出不同的反应。文件不存在通常没关系(也许是第一次运行,从空白开始就行);而“权限被拒绝”就是需要报告的真问题了。

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。

用 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 返回出去。

先写一行标题,让日志文件从头开始,然后用 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