Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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!