13. 文件操作
13.1 简介
计算机内存是一种临时存储介质,断电后数据即会丢失,且成本较高、容量有限,而磁盘属于持久化存储介质(如硬盘、SSD、U 盘等),数据可长期保存,即使断电也不会丢失,具有容量大、成本低、但读写速度相对较慢。通过文件输入输出(File Input/Output, I/O)操作,可以将数据写入磁盘,实现长期保存,确保数据在断电或重启后依然可用。常见的文件与目录操作包括创建、删除、读取和写入等。
13.2 文件路径
在进行任何文件或目录操作前,都必须先确定其位置,计算机中用于描述该位置的信息称为文件路径(File Path)。需要注意的是,Windows 与 Linux/MacOS 操作系统的文件路径格式不一样。
- Windows 系统路径格式: 所有路径从盘符(如
C:、D:)开始,并使用反斜杠\作为路径分隔符,例如C:\Users\Alice\main.rs。 - Linux/macOS 系统路径格式: 所有路径从统一的根目录
/开始,并使用正斜杠/作为路径分隔符,例如/home/alice/main.rs。
13.2.1 Path 和 PathBuf
由于不同操作系统对文件路径的表示方式存在差异,为屏蔽平台间的路径格式差异,Rust 标准库提供了 Path 和 PathBuf 两种跨平台的路径类型,用于表示和处理文件系统路径。其中,Path 用于以只读方式访问和操作路径;PathBuf 则支持路径的创建、拼接和修改,可以动态追加或修改路径内容,更适合用于构造和变更路径。
use std::path::{Path, PathBuf};
fn main() {
// 创建一个只读路径对象 Path
let path = Path::new("/home/alice/test.txt");
// 获取路径信息,Path 和 PathBuf 都支持
println!("是否存在: {:?}", path.exists());
println!("是否是文件: {:?}", path.is_file());
println!("是否是目录: {:?}", path.is_dir());
println!("名称: {:?}", path.file_name());
println!("父目录: {:?}", path.parent());
println!("扩展名: {:?}", path.extension());
println!("不含扩展名: {:?}", path.file_stem());
// 路径拼接时会自动处理平台分隔符,Winodws 下使用 \,Linux/MacOS 下使用 /
let base = Path::new("home");
let full = base.join("alice").join("test.txt");
println!("Path 拼接结果: {}", full.display());
// 创建一个空的 PathBuf 用于构建路径, 拼接时会自动处理平台分隔符
let mut path_buf = PathBuf::new();
path_buf.push("home");
path_buf.push("alice");
path_buf.push("test.txt");
println!("PathBuf 拼接结果: {}", path_buf.display());
// Path 和 PathBuf 相互转换
let path = path_buf.as_path();
let path_buf = PathBuf::from(path);
println!("path={}", path.display());
println!("path_buf={}", path_buf.display());
}
shell> cargo run
是否存在: false
是否是文件: false
是否是目录: false
名称: Some("test.txt")
父目录: Some("/home/alice")
扩展名: Some("txt")
不含扩展名: Some("test")
Path 拼接结果: home/alice/test.txt
PathBuf 拼接结果: home/alice/test.txt
path=home/alice/test.txt
path_buf=home/alice/test.txt
13.2.2 绝对路径和相对路径
文件路径根据是否依赖当前工作目录来定位目标文件或目录,分为绝对路径(Absolute Path)和相对路径(Relative Path)。其中,绝对路径从文件系统的根目录或卷标等固定位置开始,不依赖当前工作目录;相对路径则以当前工作目录为基准,用于定位目标文件或目录。
-
绝对路径:从文件系统的根目录或卷标等固定位置开始,完整描述文件或目录的位置,不受当前工作目录影响。例如 C:\Users\Alice\main.rs`。
-
相对路径:以当前工作目录为基准描述文件或目录的位置,不包含完整的根目录信息,路径会随工作目录变化。
.表示当前目录,..表示上级目录。
use std::fs;
use std::path::Path;
fn main() {
// 绝对路径:从根目录开始,不依赖当前工作目录
let abs_path = Path::new("/file/rust/project/app/src/main.rs");
println!("绝对路径: {}", abs_path.display());
// 相对路径,./ 表示当前路径,./ 可以省略
let rel_path = Path::new("./src").join("main.rs");
println!("相对路径: {}", rel_path.display());
// 相对路径转绝对路径,目标路径必须存在
let abs_path = fs::canonicalize(rel_path).unwrap();
// Windows 系统下,绝对路径会以 \\?\ 开头,需要去掉
let abs_path = abs_path.to_string_lossy()
.trim_start_matches(r"\\?\")
.to_string();
println!("相对路径转绝对路径: {}", abs_path);
}
shell> cargo run
绝对路径: /file/rust/project/app/src/main.rs
相对路径: ./src/main.rs
相对路径转绝对路径: /file/rust/project/app/src/main.rs
通过 env::current_dir() 函数获取当前工作目录,通过 env::set_current_dir() 函数修改当前工作目录。
use std::{env, fs};
use std::path::Path;
fn main() {
// 获取当前工作目录
let current_dir = env::current_dir().expect("获取当前工作目录失败");
println!("当前工作目录: {:?}", current_dir);
// 修改工作目录
let new_dir = Path::new("src");
env::set_current_dir(&new_dir)
.map(|_|println!("修改工作目录成功"))
.expect("无法修改工作目录");
// 相对路径的工作目录,变为修改后的工作目录
let rel_path = Path::new(".");
let abs_path = fs::canonicalize(&rel_path).unwrap();
println!("新工作目录: {:?}", abs_path);
}
shell> cargo run
当前工作目录: "/file/rust/project/app"
新工作目录: "src"
新工作目录: "/file/rust/project/app/src"
13.3 目录操作
目录(Directory)也称为文件夹,用于组织和管理文件,通过目录可以将不同类型或用途的文件分类存放,使文件结构更加清晰,便于查找和管理。Rust 标准库在 std::fs 模块中提供了丰富的目录操作函数,用于创建、删除、遍历目录以及读取目录内容等。
13.3.1 创建和删除目录
Rust 标准库 std::fs 模块提供了丰富的目录操作函数。其中,create_dir() 函数用于创建单级目录;create_dir_all() 函数用于递归创建多级目录,如果父目录不存在,会自动创建所需的父目录;remove_dir() 函数用于删除空目录;remove_dir_all() 函数则会递归删除指定目录及其包含的所有文件和子目录。
由于大部分文件和目录操作函数的路径参数使用 AsRef<Path> 作为泛型约束,因此除了 Path 和 PathBuf 外,还可以传入实现了 AsRef<Path> 的其他类型,例如 &str 和 String。Rust 会通过 AsRef trait 获取这些类型对应的 Path 引用,并以此进行路径处理。
use std::fs;
use std::path::Path;
use std::thread;
use std::time::Duration;
fn main() {
let dir1_path = Path::new("./data");
let dir2_path = Path::new("./temp/cache");
// 创建单级目录,如果目录已存在,则返回错误
fs::create_dir(dir1_path)
.map(|_| println!("创建单级目录成功: {}", dir1_path.display()))
.expect("创建单级目录失败");
// 递归创建多级目录,如果父目录不存在,也会一并创建
fs::create_dir_all(dir2_path)
.map(|_| println!("创建多级目录成功: {}", dir2_path.display()))
.expect("创建多级目录失败");
// 等待 10 秒后删除
println!("10 秒后删除:{:?}", fs::canonicalize(dir1_path).unwrap());
println!("10 秒后删除:{:?}", fs::canonicalize(dir2_path).unwrap());
thread::sleep(Duration::from_secs(10));
// 删除空目录,不为空则删除失败;直接传入 &str,Rust 会自动将其转换为 Path 类型
fs::remove_dir("./data")
.map(|_| println!("删除空目录成功: {}", dir1_path.display()))
.expect("删除空目录失败");
// 递归删除目录及其内部所有文件和子目录,如果目录不存在,则返回错误
fs::remove_dir_all(String::from("./temp"))
.map(|_| println!("删除目录成功: ./temp"))
.expect("删除目录失败");
}
shell> cargo run
创建单级目录成功: ./data
创建多级目录成功: ./temp/cache
10 秒后删除:"/file/rust/project/app/data"
10 秒后删除:"/file/rust/project/app/temp/cache"
删除空目录成功: ./data
删除目录成功: ./temp
13.3.2 重命名操作
Rust 标准库 std::fs 模块提供的 rename() 函数用于重命名文件或目录,也可以将文件或目录移动到同一文件系统中的其他位置,但不能跨文件系统移动。
use std::fs;
fn main() {
// 创建目录
let dir_path = "./books1";
fs::create_dir(dir_path)
.map(|_| println!("目录创建成功"))
.expect("目录创建失败");
// 重命名目录
fs::rename(dir_path, "./books2")
.map(|_| println!("目录重命名成功"))
.expect("目录重命名失败");
// 移动目录(在同一文件系统内)
fs::rename("./books2", "../books3")
.map(|_| println!("目录移动成功"))
.expect("目录移动失败");
}
shell> cargo run
目录创建成功
目录重命名成功
目录移动成功
# 查看移动后的目录
shell> ls -ld ../books3
drwxrwxr-x 2 ken ken 4096 5月 30 15:57 ../books3
13.3.3 遍历目录
遍历目录可以让程序获取目录中包含的文件和子目录信息,以便进行后续处理,例如显示目录列表、搜索特定文件、统计文件数量、计算目录大小或批量重命名等。Rust 标准库 std::fs 模块提供了 read_dir() 函数,用于读取指定目录的内容。该函数返回一个迭代器,迭代器中的每个元素都是 Result<DirEntry> 类型,表示目录中的一个条目(文件或子目录)。
use std::fs;
use std::fs::ReadDir;
fn main() {
// 读取目录
let entries: ReadDir = fs::read_dir(".").unwrap();
// 遍历目录
entries.for_each(|entry| { entry
.map(|item| println!("{:?}", item.path()))
.unwrap_or_else(|err| eprintln!("读取条目失败: {}", err));
});
}
shell> cargo run
"./src"
"./.gitignore"
"./Cargo.toml"
"./Cargo.lock"
"./target"
13.4 文件操作
文件是操作系统管理和存储数据的基本单位,可以用于保存文本、图像、音频、可执行程序等各种类型的数据。与用于组织和管理文件的目录不同,文件主要负责存储实际的数据内容。Rust 标准库通过 std::fs 模块和 std::fs::File 类型提供了丰富的文件操作接口,涵盖文件的创建、删除、打开、读写、重命名以及元数据获取等常见操作。
13.4.1 创建和删除文件
Rust 标准库 std::fs::File 类型提供的 File::create() 函数用于创建文件;如果目标文件已经存在,则会清空其原有内容。OpenOptions 类型提供了更加灵活的文件打开和创建方式,可以根据需要设置创建、清空、追加、读写权限等选项。std::fs 模块提供的 remove_file() 函数则用于删除指定文件。
use std::fs;
use std::fs::{File, OpenOptions};
fn main() {
// 创建一个空文件,如果文件已存在,则会自动清空原有内容
File::create("1.txt")
.map(|file| println!("文件创建成功, {:?}", file))
.expect("创建失败");
// 使用 OpenOptions 创建文件,并通过相应配置实现与 File::create 相同的功能
OpenOptions::new()
.write(true) // 可写
.create(true) // 创建文件
.truncate(true) // 清空原有内容
// .append(true) // 追加模式,将后续写的内容追加到文件最后
.open("2.txt")
.map(|file| println!("文件创建成功, {:?}", file))
.unwrap_or_else(|err| println!("创建失败, {:?}", err));
// 文件不存在时才创建
OpenOptions::new()
.write(true) // 可写
.create_new(true) // 创建文件, 文件已存在时返回错误
.open("2.txt") // 2.txt 文件已存在, 所以返回错误
.map(|file| println!("文件创建成功, {:?}", file))
.unwrap_or_else(|err| println!("创建失败, {:?}", err));
// 删除文件
fs::remove_file("1.txt")
.map(|_| println!("文件删除成功"))
.unwrap_or_else(|err| println!("创建失败, {:?}", err));
}
shell> cargo run
文件创建成功, File {fd: 3, path: "/file/rust/project/app/1.txt", read: false, write: true }
文件创建成功, File {fd: 3, path: "/file/rust/project/app/2.txt", read: false, write: true }
创建失败, Os { code: 17, kind: AlreadyExists, message: "File exists" }
文件删除成功
13.4.2 获取文件信息
在实际开发中,除了创建、删除和读写文件外,还经常需要获取文件的相关信息,例如文件大小、创建时间、修改时间以及文件类型等。Rust 标准库 std::fs 模块提供了 metadata() 函数,用于获取指定文件或目录的元数据(Metadata),并返回 Metadata 类型的对象。通过 Metadata 提供的函数,可以进一步获取文件大小、文件类型以及创建时间、修改时间等信息。
use std::fs;
use std::time::SystemTime;
fn main() {
// 获取文件信息
let metadata = fs::metadata("Cargo.toml")
.expect("获取文件信息失败");
// 文件大小(单位:字节)
println!("文件大小: {} bytes", metadata.len());
// 文件类型
println!("是否为文件: {}", metadata.is_file());
println!("是否为目录: {}", metadata.is_dir());
// 获取文件权限
let permissions = metadata.permissions();
println!("是否只读: {}", permissions.readonly());
// 文件创建时间(部分平台可能不支持)
if let Ok(time) = metadata.created() {
let secs = time.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
println!("创建时间: {secs} 秒");
}
// 文件最后修改时间
if let Ok(time) = metadata.modified() {
let secs = time.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
println!("最后修改时间: {secs} 秒");
}
// 文件最后访问时间
if let Ok(time) = metadata.accessed() {
let secs = time.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
println!("最后访问时间: {secs} 秒");
}
}
shell> cargo run
文件大小: 97 bytes
是否为文件: true
是否为目录: false
是否只读: false
创建时间: 1778082659 秒
最后修改时间: 1778343768 秒
最后访问时间: 1780154057 秒
13.4.3 文本文件读写
在 Rust 中,文件读写通常分为文本文件读写和二进制文件读写,文本文件读写会将文件内容按照字符编码解析为字符串,通常适用于日志、配置文件、CSV、JSON 等文本数据;二进制文件读写则直接处理原始字节数据,不进行文本编码解析,适用于图片、音频、视频等非文本数据。
通过 std::fs 模块提供的 read_to_string() 函数,可以一次性读取整个文件,并将内容作为 String 返回。由于该函数会将整个文件加载到内存中,因此通常适用于体积较小的文本文件。write() 函数则可以将数据一次性写入文件:如果目标文件已存在,会覆盖其原有内容,如果文件不存在,则自动创建。
use std::fs;
fn main() {
let path = "1.txt";
fs::write(path, "Hello, Rust")
.map(|_| println!("已写入 1.txt"))
.expect("写入文件失败");
let content = fs::read_to_string(path)
.expect("读取文件失败");
println!("文件内容: {content}");
}
shell> cargo run
已写入 1.txt
文件内容: Hello, Rust
另外,Rust 还提供了更加灵活的文件操作类型 std::fs::File,支持从指定位置读取和分批写入文件,并可对文件的打开方式进行更细粒度的控制,例如只读、追加写入或读写等模式。std::fs::read_to_string() 和 std::fs::write() 是对 File 操作的封装,用于简化一次性读取文件全部内容或一次性写入整个文件的操作。
use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
fn main() -> std::io::Result<()> {
// 打开文件
let mut file = OpenOptions::new()
.read(true) // 可读,否则不能读取
.write(true) // 可写,否则不能写入
.create(true) // 如果文件不存在,则创建文件
.append(true) // 追加模式,不会清空原文件,后续写的内容会追加到文件最后
.open("1.txt")?;
// 写入文件
file.write_all("Hello, Rust!".as_bytes())?;
// 写入后文件指针被移动到末尾,读取前需先重置到开头
file.seek(SeekFrom::Start(0))?;
// 读取整个文件
let mut contents = String::new();
let size = file.read_to_string(&mut contents)?;
println!("size: {}, contents: {}", size, contents);
Ok(())
}
shell> cargo run
size: 12, contents: Hello, Rust!
对于较大文件或需要逐行处理的文本文件,可以使用 std::io::BufReader 配合 lines() 方法按行读取,避免一次性将整个文件加载到内存中,降低内存占用;写入文件时,可以使用 std::io::BufWriter 进行缓冲写入,将多次写入操作暂存于内存缓冲区,并在缓冲区写满或显式调用 flush() 时,将缓冲区中的数据写入文件,从而减少磁盘 I/O 次数,提高写入效率。
use std::fs::OpenOptions;
use std::io::{self, BufRead, BufReader, BufWriter, Seek, SeekFrom, Write};
fn main() -> io::Result<()> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open("1.txt")?;
// 通过缓冲区写入
let mut writer = BufWriter::with_capacity(8 * 1024, file);
writer.write_all("我是第一行\n".as_bytes())?;
writer.write_all("我是第二行\n".as_bytes())?;
writer.write_all("我是第三行\n".as_bytes())?;
// 手动刷新缓冲区,确保数据写入磁盘,
writer.flush()?;
// 写入后文件指针被移动到末尾,读取前需先重置到开头
let mut file = writer.into_inner()?;
file.seek(SeekFrom::Start(0))?;
// 逐行读取文件
let reader = BufReader::with_capacity(8 * 1024, file);
for line in reader.lines() {
println!("{}", line?);
}
Ok(())
}
shell> cargo run
我是第一行
我是第二行
我是第三行
13.4.4 二进制文件读写
二进制文件中的数据由连续的字节(Byte)组成,读写时以字节为单位,无需编码转换,常用于存储图片、音视频、数据库文件以及需要精确控制数据布局的结构化数据等场景。
通过 std::fs::read() 可一次性读取整个二进制文件,将内容以 Vec<u8> 形式返回。由于该函数会将完整文件加载到内存,通常仅适用于体积较小的二进制文件。std::fs::write() 则可将字节数据一次性写入文件:若目标文件已存在,则覆盖原有内容;若不存在,则自动创建。
use std::fs;
fn main() {
let path = "data.bin";
// 写入二进制数据,如果目标文件已存在则会覆盖原有内容,不存在则会自动创建
let data: [u8; 5] = [1, 2, 3, 4, 5];
fs::write(path, data)
.expect("写入文件失败");
// 读取二进制数据
let bytes = fs::read(path)
.expect("读取文件失败");
println!("{:?}", bytes);
}
shell> cargo run
[1, 2, 3, 4, 5]
除一次性读取整个文件外,还可采用分块读取策略,每次将固定大小的数据载入缓冲区处理。相比一次性加载完整内容,分块读取能显著降低内存占用,更适合处理大型二进制文件或需要流式处理数据的场景。
use std::fs::{self, File};
use std::io::Read;
fn main() {
let path = "data.bin";
// 创建测试文件
let data: Vec<u8> = (1..=50).collect();
fs::write(path, &data)
.expect("写入文件失败");
let mut file = File::open(path)
.expect("打开文件失败");
// 创建缓冲区,大小为 10 字节
let mut buffer = [0u8; 10];
// 分块读取
while let Ok(size) = file.read(&mut buffer) {
if size == 0 {
break;
}
println!("{:?}", &buffer[..size]);
}
}
shell> cargo run
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
[21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
[31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
[41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
另外,二进制文件的读写也可以借助 BufReader 和 BufWriter 实现,它们自带缓冲区,既支持按字节读取,也支持按块读取数据。
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
fn write_file_by_chunks(path: &str) -> std::io::Result<()> {
// 打开文件并创建带缓冲区的写入器
let file = File::create(path)?;
let mut writer = BufWriter::with_capacity(4096, file);
// 分块写入数据,每次写入 10 个字节
let data: Vec<u8> = (1..=20).collect();
for chunk in data.chunks(10) {
writer.write_all(chunk)?;
println!("写入块:{:?}", chunk);
}
// 显式刷新缓冲区,确保所有数据写入磁盘
writer.flush()?;
Ok(())
}
fn read_in_bytes(path: &str) -> std::io::Result<()> {
let file = File::open(path)?;
// 创建带缓冲区的读取器
let reader = BufReader::with_capacity(4096, file);
// 逐字节读取文件内容并打印
for byte in reader.bytes() {
print!("{:?} ", byte?);
}
Ok(())
}
fn read_in_chunks(path: &str) -> std::io::Result<()> {
let file = File::open(path)?;
// 创建带缓冲区的读取器
let mut reader = BufReader::with_capacity(4096, file);
// 按块读取文件内容,每次读取 buffer 大小的字节
while let Ok(buffer) = reader.fill_buf() {
if buffer.is_empty() {
break; // EOF
}
println!("{:?}", buffer);
// 消耗掉已读取的缓冲区数据,以便下次 fill_buf 读取新内容
let len = buffer.len();
reader.consume(len);
}
Ok(())
}
fn main() -> std::io::Result<()> {
let path = "data.bin";
// 分块写入文件
write_file_by_chunks(path)?;
// 按字节读取
println!("按字节读取");
read_in_bytes(path)?;
// 按块读取
println!("\n按块读取");
read_in_chunks(path)?;
Ok(())
}
shell> cargo run
写入块:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
写入块:[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
按字节读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
按块读取
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
13.4.5 虚拟内存映射文件
对于大文件读写,每次读写操作都需要经过操作系统,并在用户空间和内核空间之间进行数据复制。为了进一步提高文件访问效率,现代操作系统提供了虚拟内存映射文件(Memory-Mapped File)机制。该机制将文件映射到进程的一块内存区域,使程序可以像访问内存一样直接访问文件内容,由操作系统负责数据加载、缓存以及修改后的同步,从而减少数据复制和系统调用开销。从本质上看,虚拟内存映射文件并不是将整个文件一次性加载到内存,而是建立文件与虚拟内存地址空间之间的映射关系。程序访问映射区域时,操作系统通过分页机制按需加载数据,仅将实际访问的数据加载到物理内存中。
Rust 标准库没有提供虚拟内存映射文件相关接口,需要使用第三方库实现,例如 memmap2。该库通过调用操作系统提供的内存映射接口,将文件映射到进程地址空间,并提供安全的 Rust 封装,使开发者能够以切片(slice)的方式访问文件内容。
use memmap2::{Mmap, MmapMut};
use std::fs::OpenOptions;
use std::io::Result;
fn main() -> Result<()> {
write_file()?;
read_file()?;
Ok(())
}
// 写文件
fn write_file() -> Result<()> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open("data.txt")?;
// 设置文件大小
file.set_len(1024)?;
// 将文件映射到虚拟内存
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
// 像访问内存一样修改文件内容
mmap[0..12].copy_from_slice(b"Hello, Rust!");
// 同步修改内容到文件
mmap.flush()?;
println!("写入成功!");
Ok(())
}
// 读文件
fn read_file() -> Result<()> {
let file = OpenOptions::new().read(true).open("data.txt")?;
// 将文件映射到虚拟内存
let mmap = unsafe { Mmap::map(&file)? };
// 像读取内存一样访问文件内容
let content = &mmap[0..12];
println!("{}", String::from_utf8_lossy(content));
Ok(())
}
shell> cargo add memmap2@0.9.10
shell> cargo run
写入成功!
Hello, Rust!