在 Rust 中,文件操作主要依赖标准库的 std::fs
(文件系统核心功能)和 std::path
(路径处理)模块,辅以 std::io
(输入输出 traits)实现高效读写。本文将从基础到进阶,全面覆盖 Rust 文件操作的核心场景、代码示例及最佳实践。
一、核心模块与前置知识
在开始前,需了解三个核心模块的定位:
模块 | 核心作用 | 关键类型 / 函数 |
---|---|---|
std::fs |
文件 / 目录的创建、删除、读写、元数据获取 | File 、read_dir 、create_dir |
std::path |
路径的构建、拼接、转换(跨平台兼容) | Path (不可变路径)、PathBuf (可变路径) |
std::io |
定义 IO 操作的通用 traits(如 Read /Write ) |
BufReader 、BufWriter 、Error |
基础准备
Rust 标准库无需额外依赖,直接在代码中引入即可:
use std::fs;
use std::path::{Path, PathBuf};
use std::io::{self, Read, Write, BufReader, BufWriter};
// 错误处理常用(Box<dyn Error> 可捕获大多数错误)
use std::error::Error;
注意:文件操作几乎所有函数都返回 Result<T, E>
(避免 panic),需通过 ?
、match
或 if let
处理错误。下文示例中,main
函数会返回 Result<(), Box<dyn Error>>
以简化错误处理。
二、路径操作(std::path
)
路径是文件操作的基础,Rust 提供 Path
(不可变)和 PathBuf
(可变,类似 String
)两种类型,自动处理跨平台路径分隔符(Windows \
、Unix /
)。
1. 路径创建
- 从字符串字面量创建
Path
(不可变):
let path: &Path = Path::new("./test.txt"); // 相对路径
let abs_path: &Path = Path::new("/home/user/test.txt"); // 绝对路径(Unix)
- 创建
PathBuf
(可变,支持拼接):
// 方式1:从 Path 转换
let mut path_buf = PathBuf::from(path);
// 方式2:直接从字符串创建
let mut path_buf = PathBuf::from("./docs");
2. 路径拼接(核心操作)
使用 push
(追加路径段)或 join
(创建新路径):
fn main() -> Result<(), Box<dyn Error>> {
let mut base = PathBuf::from("./data");
// 拼接:./data/logs/2024.txt
base.push("logs");
base.push("2024.txt");
println!("拼接后路径:{}", base.display()); // display() 用于友好打印路径
// 另一种方式:join(不修改原路径,返回新 PathBuf)
let new_path = PathBuf::from("./data").join("logs").join("2024.txt");
println!("join 路径:{}", new_path.display());
Ok(())
}
3. 路径转换与判断
- 转换为字符串(需处理非 UTF-8 路径,Rust 路径允许非 UTF-8 字符):
let path = PathBuf::from("./test.txt");
// 安全转换(非 UTF-8 时返回 None)
if let Some(s) = path.to_str() {
println!("路径字符串:{}", s);
} else {
eprintln!("路径包含非 UTF-8 字符");
}
- 判断路径属性:
let path = Path::new("./test.txt");
println!("是否存在:{}", path.exists());
println!("是否为文件:{}", path.is_file());
println!("是否为目录:{}", path.is_dir());
println!("是否为绝对路径:{}", path.is_absolute());
三、文件基础操作(std::fs
)
涵盖文件的创建、写入、读取、删除、重命名等核心场景。
1. 创建文件
- 方式 1:
fs::File::create
(不存在则创建,存在则覆盖):
fn main() -> Result<(), Box<dyn Error>> {
// 创建文件(返回 File 句柄,可用于后续写入)
let mut file = fs::File::create("./new_file.txt")?;
// 写入内容(需实现 Write trait)
file.write_all(b"Hello, Rust File!")?; // b"" 表示字节流
Ok(())
}
- 方式 2:
fs::OpenOptions
(更灵活的创建 / 打开配置,如追加、只读):
fn main() -> Result<(), Box<dyn Error>> {
// 配置:追加模式(不覆盖原有内容),不存在则创建
let mut file = fs::OpenOptions::new()
.append(true) // 追加
.create(true) // 不存在则创建
.open("./log.txt")?;
file.write_all(b"\nAppend new line!")?;
Ok(())
}
2. 读取文件
根据文件大小选择不同读取方式,避免内存浪费。
(1)一次性读取(小文件推荐)
- 读取为字节向量:
fs::read
- 读取为字符串:
fs::read_to_string
(自动处理 UTF-8 编码)
fn main() -> Result<(), Box<dyn Error>> {
// 读取为字符串(小文件)
let content = fs::read_to_string("./test.txt")?;
println!("文件内容:\n{}", content);
// 读取为字节(二进制文件,如图片、音频)
let bytes = fs::read("./image.png")?;
println!("图片大小:{} 字节", bytes.len());
Ok(())
}
(2)缓冲读取(大文件推荐)
大文件一次性读取会占用大量内存,需用 BufReader
按块 / 按行读取:
fn main() -> Result<(), Box<dyn Error>> {
// 打开文件并包装为缓冲读取器
let file = fs::File::open("./large_file.txt")?;
let reader = BufReader::new(file);
// 按行读取(高效,逐行加载到内存)
for line in reader.lines() {
let line = line?; // 处理每行的读取错误
println!("行内容:{}", line);
}
// 按块读取(自定义缓冲区大小)
let mut file = fs::File::open("./large_file.txt")?;
let mut reader = BufReader::with_capacity(1024 * 1024, file); // 1MB 缓冲区
let mut buf = [0; 1024]; // 每次读取 1KB
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break; // 读取结束
}
println!("读取 {} 字节:{:?}", n, &buf[..n]);
}
Ok(())
}
3. 写入文件
(1)一次性写入(小内容推荐)
fs::write
简化创建 + 写入流程(内部自动处理文件打开和关闭):
fn main() -> Result<(), Box<dyn Error>> {
// 写入字符串(自动转换为字节)
fs::write("./test.txt", "Hello, fs::write!")?;
// 写入字节(二进制内容)
fs::write("./binary.data", b"raw bytes")?;
Ok(())
}
(2)缓冲写入(频繁写入推荐)
BufWriter
减少 IO 系统调用次数,提升写入效率(尤其适合频繁小写入):
fn main() -> Result<(), Box<dyn Error>> {
let file = fs::File::create("./buffered_write.txt")?;
let mut writer = BufWriter::new(file); // 默认缓冲区大小,也可自定义 with_capacity
// 多次写入(实际会先缓冲,满了再刷盘)
writer.write_all(b"First line\n")?;
writer.write_all(b"Second line\n")?;
writer.flush()?; // 手动刷盘(确保内容写入磁盘,BufWriter 析构时也会自动刷盘)
Ok(())
}
4. 文件删除与重命名
- 删除文件:
fs::remove_file
(仅删除文件,删除目录需用remove_dir
) - 重命名文件:
fs::rename
(跨目录移动文件也可用此函数)
fn main() -> Result<(), Box<dyn Error>> {
// 重命名:将 old.txt 改为 new.txt
fs::rename("./old.txt", "./new.txt")?;
// 删除文件(若文件不存在,会返回 NotFound 错误)
if Path::new("./new.txt").exists() {
fs::remove_file("./new.txt")?;
println!("文件已删除");
}
Ok(())
}
四、目录操作(std::fs
)
目录操作包括创建、读取、删除、复制等,需注意目录是否为空的区别。
1. 创建目录
- 单个目录:
fs::create_dir
(父目录不存在则报错) - 递归创建目录(含父目录):
fs::create_dir_all
(推荐,类似mkdir -p
)
fn main() -> Result<(), Box<dyn Error>> {
// 递归创建:./data/logs/2024(父目录 data、logs 不存在则自动创建)
fs::create_dir_all("./data/logs/2024")?;
println!("目录创建成功");
Ok(())
}
2. 读取目录内容
fs::read_dir
返回目录条目迭代器,每个条目是 DirEntry
(含路径和元数据):
fn main() -> Result<(), Box<dyn Error>> {
let dir_path = Path::new("./data");
// 读取目录条目
let entries = fs::read_dir(dir_path)?;
for entry in entries {
let entry = entry?; // 处理条目读取错误
let path = entry.path();
// 获取条目类型(文件/目录)
if path.is_file() {
println!("文件:{}", path.display());
} else if path.is_dir() {
println!("目录:{}", path.display());
}
// 获取文件大小(通过元数据)
let metadata = entry.metadata()?;
println!(" 大小:{} 字节", metadata.len());
}
Ok(())
}
3. 删除目录
- 删除空目录:
fs::remove_dir
(目录非空则报错) - 删除非空目录(含所有子内容):
fs::remove_dir_all
(危险!需谨慎使用,类似rm -rf
)
fn main() -> Result<(), Box<dyn Error>> {
// 删除空目录
if Path::new("./empty_dir").is_dir() {
fs::remove_dir("./empty_dir")?;
}
// 删除非空目录(谨慎!会删除所有子文件/目录)
if Path::new("./data").is_dir() {
fs::remove_dir_all("./data")?;
println!("非空目录已删除");
}
Ok(())
}
4. 复制目录(标准库无原生函数,需手动实现)
Rust 标准库未提供 copy_dir
,需递归复制目录下的所有文件和子目录。可借助第三方库 walkdir
简化遍历(见下文进阶部分),或手动实现:
fn copy_dir(src: &Path, dst: &Path) -> Result<(), Box<dyn Error>> {
// 创建目标目录
fs::create_dir_all(dst)?;
// 遍历源目录
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name()); // 保持原文件名
if src_path.is_file() {
// 复制文件
fs::copy(&src_path, &dst_path)?;
println!("复制文件:{} -> {}", src_path.display(), dst_path.display());
} else if src_path.is_dir() {
// 递归复制子目录
copy_dir(&src_path, &dst_path)?;
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
copy_dir(Path::new("./src_dir"), Path::new("./dst_dir"))?;
Ok(())
}
五、文件元数据(fs::metadata
)
元数据包含文件大小、修改时间、权限、类型等信息,通过 fs::metadata
或 DirEntry::metadata
获取:
fn main() -> Result<(), Box<dyn Error>> {
let path = Path::new("./test.txt");
let metadata = fs::metadata(path)?;
// 基本信息
println!("是否为文件:{}", metadata.is_file());
println!("是否为目录:{}", metadata.is_dir());
println!("文件大小:{} 字节", metadata.len());
// 修改时间(SystemTime 类型,需转换为可读格式)
let mtime = metadata.modified()?;
println!("最后修改时间:{:?}", mtime);
// 权限(跨平台格式不同,Unix 为 rwx,Windows 为权限掩码)
let permissions = metadata.permissions();
#[cfg(unix)]
println!("Unix 权限:{:?}", permissions.mode()); // 如 0o644
#[cfg(windows)]
println!("Windows 只读:{}", permissions.read_only());
Ok(())
}
六、错误处理(Rust 文件操作的核心)
文件操作的错误类型主要是 std::io::Error
,包含错误码(如 NotFound
、PermissionDenied
)和描述。处理方式有三种:
1. 用 ?
快速传播错误(推荐)
?
会自动将 Result
中的错误转换为函数返回类型(需函数返回 Result
),适合简单场景:
fn read_file(path: &str) -> Result<String, Box<dyn Error>> {
let content = fs::read_to_string(path)?; // 错误直接传播
Ok(content)
}
2. 用 match
精细处理错误
适合需要根据错误类型分支处理的场景(如 “文件不存在则创建”):
fn read_or_create(path: &str) -> Result<String, Box<dyn Error>> {
match fs::read_to_string(path) {
Ok(content) => Ok(content),
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
// 文件不存在,创建并写入默认内容
fs::write(path, "default content")?;
Ok("default content".to_string())
} else {
// 其他错误传播
Err(e.into())
}
}
}
}
3. 自定义错误类型(复杂项目推荐)
使用 thiserror
crate 定义业务相关的错误类型,提升可读性:
- 在
Cargo.toml
中添加依赖:
[dependencies]
thiserror = "1.0"
- 定义自定义错误:
use thiserror::Error;
#[derive(Error, Debug)]
enum FileOpError {
#[error("文件未找到:{0}")]
FileNotFound(String),
#[error("权限不足:{0}")]
PermissionDenied(String),
#[error("IO 错误:{0}")]
IoError(#[from] std::io::Error),
}
// 使用自定义错误
fn read_file(path: &str) -> Result<String, FileOpError> {
let content = fs::read_to_string(path)?; // io::Error 自动转换为 FileOpError
Ok(content)
}
fn main() {
match read_file("./nonexistent.txt") {
Ok(_) => println!("读取成功"),
Err(e) => eprintln!("错误:{}", e), // 输出:错误:IO 错误:No such file or directory (os error 2)
}
}
七、进阶操作(第三方库)
标准库已覆盖大部分基础场景,但复杂需求(如目录树遍历、内存映射、文件锁)需借助第三方库。
1. 目录树遍历(walkdir
)
walkdir
简化递归遍历目录树,支持过滤、深度限制等功能:
- 依赖:
walkdir = "2"
- 示例(遍历所有
.txt
文件):
use walkdir::WalkDir;
fn main() {
// 遍历 ./data 下所有文件,深度不超过 3
for entry in WalkDir::new("./data").max_depth(3) {
let entry = entry.unwrap();
let path = entry.path();
// 过滤 .txt 文件
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("txt") {
println!("TXT 文件:{}", path.display());
}
}
}
2. 内存映射文件(memmap2
)
将文件直接映射到内存,避免拷贝,适合大文件随机访问:
- 依赖:
memmap2 = "0.9"
- 示例:
use memmap2::Mmap;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let file = File::open("./large_file.txt")?;
// 映射整个文件到内存(只读)
let mmap = unsafe { Mmap::map(&file)? }; // unsafe:需确保文件不被修改
// 直接访问内存中的内容(类似字节切片)
println!("前 100 字节:{:?}", &mmap[0..100]);
Ok(())
}
3. 文件锁(fslock
)
处理多进程 / 线程并发写文件的场景,避免数据竞争:
- 依赖:
fslock = "0.2"
- 示例(加锁写入):
use fslock::LockFile;
use std::fs::OpenOptions;
fn main() -> Result<(), Box<dyn Error>> {
let file = OpenOptions::new()
.write(true)
.create(true)
.open("./locked.txt")?;
let mut lock = LockFile::open(file)?;
lock.lock()?; // 加排他锁(其他进程无法同时写入)
// 写入内容
writeln!(lock, "并发安全写入")?;
lock.unlock()?; // 释放锁(也可自动释放,因 LockFile 析构时会解锁)
Ok(())
}
八、最佳实践
- 优先使用缓冲 IO:大文件 / 频繁读写时,用
BufReader
/BufWriter
减少系统调用,提升性能。 - 避免
fs::remove_dir_all
:除非明确需要删除非空目录,否则优先检查目录是否为空,防止误删。 - 路径处理用
PathBuf
:避免手动拼接字符串(如"./data/" + "logs"
),PathBuf
自动处理跨平台分隔符。 - 错误处理要充分:不要用
unwrap()
忽略错误,生产环境需用?
或match
处理所有可能的错误。 - 资源自动释放:Rust 的
File
、BufReader
等类型实现了Drop
trait,离开作用域时会自动关闭文件句柄,无需手动关闭。
通过以上内容,可覆盖 Rust 文件操作的绝大多数场景。