Rust 的 I/O 基于 std::io 与 std::fs,围绕 Read/Write/Seek/BufRead 等 trait 构建出一致的抽象。I/O 天然伴随失败与延迟,
因此 Result 是主角,? 运算符是常客。本部分从控制台输入输出出发,逐步过渡到文件读写与缓冲、二进制与文本处理、路径与元数据、错误传播与稳定性,再到常见陷阱与性能建议。

Rust 提供了多种输出宏来处理不同的输出需求:
println!() 用于标准输出并自动添加换行符,是最常用的输出方式print!() 输出到标准输出但不添加换行符,适合需要连续输出或构建同一行内容的场景eprintln!() 和 eprint!() 输出到标准错误流,用于错误信息、警告或调试信息,不会与正常输出混淆格式化字符串中的占位符决定了数据的显示方式:
{} 使用类型的 Display trait 实现,提供面向用户的友好格式{:?} 使用 Debug trait 实现,提供面向开发者的调试格式,显示内部结构{:#?} 使用美化的 Debug 格式,多行缩进显示复杂结构对于自定义类型,我们可以通过 #[derive(Debug)] 自动生成 Debug 实现,或手动实现 Display 和 Debug trait 来控制输出格式。这在开发和调试过程中极其有用,能让我们清楚地看到数据结构的内容。
|#[derive(Debug)] struct Point { x: i32, y: i32 } fn main() { let p = Point { x: 3, y: 4 }; println!("p = {:?}", p); eprintln!("warn: demo only"); }
|p = Point { x: 3, y: 4 } warn: demo only
标准输出通常使用行缓冲或全缓冲机制来提高性能。这意味着我们的输出内容可能不会立即显示在终端上,而是先存储在缓冲区中,直到缓冲区满了或遇到换行符才会真正输出。
在交互式程序中,这种缓冲机制可能会导致用户体验问题。例如,当我们使用 print!() 输出提示信息(如"请输入姓名:")时,由于没有换行符,这些文字可能不会立即显示,用户会看到一个空白的终端,不知道程序在等待什么。
为了解决这个问题,我们需要手动调用 io::stdout().flush() 来强制刷新输出缓冲区,确保提示信息立即显示给用户。这在需要用户输入的场景中特别重要,能够提供更好的交互体验。
|use std::io::{self, Write}; fn main() -> io::Result<()> { print!("请输入名字: "); io::stdout().flush()?; // 立刻刷新提示 Ok(()) }
标准输入提供了两种主要的读取方式,每种都有其特定的使用场景和特点。
stdin().read_line() 是处理文本输入的首选方法。它会从输入流中读取一整行内容,直到遇到换行符(\n)为止。需要注意的是,这个方法会将换行符也包含在读取的字符串中,因此在处理输入时通常需要使用 trim() 或 trim_end() 来移除末尾的空白字符。这种行为设计让我们能够区分是否真的读到了完整的一行。
对于需要更精细控制的场景,read() 方法提供了字节级别的读取能力。它直接读取原始字节数据到缓冲区中,不会进行任何文本解析或换行符处理。这种方式特别适合处理二进制数据、自定义编码格式,或者需要实现特殊协议的场景。与 read_line() 不同,read() 方法会返回实际读取的字节数,我们需要根据这个数值来确定有效数据的范围。
|use std::io::{self, Read, Write}; fn main() -> io::Result<()> { print!("输入一行: "); io::stdout().flush()?; let mut line = String::new(); io
文件操作是程序与存储系统交互的核心功能。在 Rust 中,std::fs::File 结构体代表了一个打开的文件句柄,它提供了对文件进行读写操作的接口。
File::create() 方法用于创建新文件或覆盖现有文件。当我们调用这个方法时,如果目标文件已经存在,它会被完全清空(截断为0字节),然后重新开始写入。如果文件不存在,则会创建一个新的空文件。创建的文件默认具有写入权限,但不能直接用于读取操作。
相对地,File::open() 方法以只读模式打开现有文件。这个方法不会修改文件内容,也不会创建新文件。如果指定的文件不存在,open() 方法会返回错误。这种设计确保了读取操作的安全性,避免意外修改文件。
当我们需要更精细的文件操作控制时,OpenOptions 结构体提供了强大的配置能力。通过链式调用不同的方法,我们可以精确指定文件的打开模式:
read(true) 启用读取权限write(true) 启用写入权限append(true) 启用追加模式,新内容会添加到文件末尾create(true) 如果文件不存在则创建create_new(true) 只有在文件不存在时才创建,存在则报错truncate(true) 打开时清空文件内容|use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Write}; fn write_and_read() -> io::Result<String> { let mut f = File::create("out.txt"
|第一行 追加一行
BufReader/BufWriter 与逐行读取在进行文件 I/O 操作时,每次读写都会触发系统调用,这在频繁的小数据操作中会造成性能瓶颈。缓冲 I/O 通过在内存中维护一个缓冲区来解决这个问题,将多次小的读写操作合并为较少的大块系统调用,从而显著提升性能。
BufReader 在读取时会预先从文件中读取一大块数据到内存缓冲区,后续的读取操作直接从缓冲区获取数据,避免频繁的系统调用。同样,BufWriter 会将写入的数据先存储在内存缓冲区中,当缓冲区满或显式调用 flush() 时才将数据批量写入文件。
特别值得注意的是 BufRead trait 提供的 lines() 方法,它返回一个迭代器,能够逐行读取文本文件。这个迭代器产生的每个元素都是 Result<String>,我们可以使用 ? 操作符来处理可能的 I/O 错误。这种方式在处理大型文本文件时非常高效,因为它不需要一次性将整个文件加载到内存中,而是按需逐行读取。
|use std::fs::File; use std::io::{self, BufRead, BufReader, BufWriter, Write}; fn main() -> io::Result<()> { let mut writer = BufWriter::new(File::create(
在处理文件 I/O 时,我们需要清楚区分文本数据和二进制数据的不同处理方式。文本数据涉及字符编码转换(如 UTF-8),而二进制数据则是原始字节序列的直接操作。
对于二进制文件的读写,我们应该优先使用 read_exact() 和 write_all() 方法。read_exact() 能够确保读取指定数量的字节,如果文件中的数据不足,它会返回错误而不是部分数据。这对于解析固定格式的二进制文件特别重要,比如图像文件头、数据库记录等。write_all() 则保证所有数据都被写入,即使在某些情况下单次 write() 调用可能只写入部分数据。
在处理复杂的二进制格式时,我们通常需要定义自定义的数据结构来映射字节布局。这可能涉及到字节序(大端序或小端序)的转换、结构体字段的对齐、以及不同数据类型在内存中的表示。
|use std::fs::File; use std::io::{self, Read, Write}; fn main() -> io::Result<()> { let data: [u8; 4] = [0x12, 0x34, 0x56, 0x78
|[12, 34, 56, 78]
Path/PathBuf、metadata、read_dir在文件系统操作中,路径处理是一个核心概念。Rust 提供了 std::path::Path 和 PathBuf 来处理跨平台的路径操作。
Path 是一个借用类型(类似 &str),而 PathBuf 是拥有类型(类似 String)。Path 用于引用现有的路径,而 PathBuf 用于构建和修改路径。这种设计让我们能够高效地处理路径字符串,同时避免不必要的内存分配。
路径处理会自动适应不同操作系统的约定:在 Windows 上使用反斜杠(\)作为分隔符,在 Unix 系统上使用正斜杠(/)。我们不需要手动处理这些差异,Rust 的路径 API 会为我们抽象这些细节。
metadata 函数能够获取文件或目录的详细信息,包括大小、修改时间、权限等。这对于文件管理、备份程序或者需要根据文件属性做决策的应用程序特别有用。元数据操作通常比读取文件内容要快得多,因为它只需要查询文件系统的索引信息。
read_dir 函数返回一个迭代器,用于遍历目录中的所有条目。每个条目都包含路径信息和基本元数据。需要注意的是,目录遍历的顺序通常是不确定的,如果需要特定顺序,我们需要手动对结果进行排序。
|use std::fs; use std::path::Path; fn main() -> std::io::Result<()> { let p = Path::new("Cargo.toml"); if p.exists() { let md = fs::metadata(p)
io::ErrorKind在实际的 I/O 操作中,我们会遇到各种各样的错误情况。理解这些错误的成因和处理方式,对于编写稳定的程序至关重要。
这里我们使用 match 语句来处理错误,并使用 io::ErrorKind 来匹配错误类型。
|use std::fs; use std::io::{self, ErrorKind}; fn read_config(path: &str) -> io::Result<String> { match fs::read_to_string(path) { Ok(s) => Ok(s), Err(e) if e
SeekSeek trait 为文件提供了强大的随机访问能力,允许我们在文件中任意移动读写位置。这个功能在许多场景下都非常有用:
Seek trait 提供了三种定位方式:SeekFrom::Start(n) 从文件开头偏移 n 字节,SeekFrom::Current(n) 从当前位置偏移 n 字节(可为负数),SeekFrom::End(n) 从文件末尾偏移 n 字节。
一个简单的示例:
|use std::fs::File; use std::io::{self, Seek, SeekFrom, Write, Read}; fn main() -> io::Result<()> { let mut f = File::create("seek.txt")?;
|abXYef
9. 标准输入输出练习
编写一个交互式程序,实现以下功能:
print!() 打印提示语"请输入姓名: "(注意:不要使用 println!(),因为后面需要立即读取输入)io::stdout().flush() 强制刷新输出缓冲区,确保提示信息立即显示io::stdin().read_line() 读取用户输入的一行文本trim_end() 或 trim() 移除输入字符串末尾的换行符和空白字符println!() 输出回显信息,格式为"你好, <name>"(其中 <name> 是用户输入的姓名)|use std::io::{self, Write}; fn main() -> io::Result<()> { // 打印提示(不换行) print!("请输入姓名: "); // 立即刷新输出缓冲区 io::stdout().flush()?; // 读取一行输入 let mut name = String
10. 文件读写与行数统计练习
编写一个程序,实现以下功能:
notes.txt 的文件writeln!() 宏,每行一个字符串)File::open() 打开刚才创建的文件BufReader::new() 包装文件句柄,创建缓冲读取器BufReader 的 lines() 方法获取行迭代器lines() 返回的每个元素是 Result<String>,需要使用 ? 处理错误)|use std::fs::File; use std::io::{self, BufRead, BufReader, Write}; fn main() -> io::Result<()> { // 创建文件并写入多行 let mut file = File::create("notes.txt")?;
11. 追加模式文件写入练习
编写一个程序,实现以下功能:
OpenOptions 以追加模式打开文件 app.logcreate(true))append(true))SystemTime 获取当前时间OpenOptions 实例,然后链式调用 append(true) 和 create(true),最后调用 open() 打开文件|use std::fs::OpenOptions; use std::io::{self, Write}; fn main() -> io::Result<()> { // 使用OpenOptions以追加模式打开文件,不存在则创建 let mut file = OpenOptions::new() .append(true) .create
12. 二进制文件读写练习
编写一个程序,实现以下功能:
data.bin 的二进制文件[0x01, 0x02, 0xFF](使用 write_all() 方法)Vec<u8>(使用 read_to_end() 方法)assert_eq!() 断言读回的数据与写入的数据完全一致println!() 打印读回的数据,格式为十六进制(可以使用 {:x?} 格式化)|use std::fs::File; use std::io::{self, Read, Write}; fn main() -> io::Result<()> { // 写入二进制数据 let data = [0x01, 0x02, 0xFF]; File::create(
13. 错误处理与默认值练习
实现一个函数 read_or_default(path: &str) -> io::Result<String>,功能如下:
fs::read_to_string(path) 读取文件内容ErrorKind::NotFound),则返回默认字符串 "default=1"Err(e))match 语句匹配 Result,在错误分支中使用 if 守卫检查错误类型|use std::fs; use std::io::{self, ErrorKind}; fn read_or_default(path: &str) -> io::Result<String> { match fs::read_to_string(path) { Ok(content) => Ok(content), Err(e) if
14. 目录遍历与文件过滤练习
编写一个程序,实现以下功能:
fs::read_dir(".") 读取当前目录的所有条目read_dir() 返回的迭代器,每个元素是 Result<DirEntry>)entry.file_type()?.is_file())entry.path()).rs 结尾(可以使用 path.extension() 和 Some("rs") 进行比较,或使用 path.to_string_lossy().ends_with(".rs"))path.display() 或 path.to_string_lossy())|use std::fs; fn main() -> std::io::Result<()> { // 读取当前目录 for entry in fs::read_dir(".")? { let entry = entry?; // 检查是否为文件 if entry.file_type()?.
15. 文件定位与随机读写练习
编写一个程序,实现以下功能:
seek_demo.txt 的文件"abcdef"(使用 write_all() 方法,注意:字符串需要转换为字节,可以使用 b"abcdef" 或 "abcdef".as_bytes())seek() 方法将文件指针移动到第三个字节的位置(使用 SeekFrom::Start(2),注意:索引从0开始,所以位置2是第三个字节)"XY"(这会覆盖原来的 "cd")read_to_string() 方法)"abXYef"|use std::fs::File; use std::io::{self, Seek, SeekFrom, Write, Read}; fn main() -> io::Result<()> { // 创建文件并写入初始内容 let mut file = File::create("seek_demo.txt")
|请输入姓名: 张三 你好, 张三
说明:
print!() 不会自动换行,适合打印提示信息flush() 确保提示立即显示,避免用户看到空白终端read_line() 会将换行符也包含在字符串中,需要使用 trim_end() 移除? 操作符处理可能的 I/O 错误|lines=3
说明:
File::create() 创建新文件或覆盖现有文件writeln!() 宏用于写入一行并自动添加换行符BufReader::new() 创建缓冲读取器,提高读取性能lines() 返回迭代器,每个元素是 Result<String>? 操作符处理可能的 I/O 错误|(程序执行后,app.log文件内容为:) 日志: 2024-01-01 12:00:00
说明:
OpenOptions::new() 创建新的配置对象append(true) 启用追加模式,新内容添加到文件末尾create(true) 如果文件不存在则创建open() 根据配置打开文件|[1, 2, ff]
说明:
write_all() 保证所有数据都被写入,适合二进制数据read_to_end() 读取文件的所有内容到 Vec<u8>assert_eq!() 用于验证数据一致性{:x?} 格式化输出为十六进制|default=1
说明:
fs::read_to_string() 一次性读取整个文件内容ErrorKind::NotFound 表示文件不存在的错误类型if 守卫(if e.kind() == ErrorKind::NotFound)匹配特定错误类型Err(e) 向上传播,保持错误信息的完整性.into() 将字符串字面量转换为 String 类型|main.rs lib.rs (实际输出取决于当前目录中的.rs文件)
说明:
read_dir(".") 读取当前目录,返回 Result<ReadDir>Result<DirEntry>,需要使用 ? 处理错误file_type()?.is_file() 检查是否为文件path.extension() 获取文件扩展名,返回 Option<&OsStr>OsStr::new("rs") 创建扩展名字符串进行比较path.display() 用于打印路径,自动处理跨平台差异|abXYef
说明:
File::create() 创建新文件或覆盖现有文件b"abcdef" 是字节字符串字面量,类型为 &[u8]seek(SeekFrom::Start(2)) 从文件开头偏移2字节,移动到第三个字节位置"XY" 会覆盖原来的 "cd"read_to_string() 读取整个文件内容到字符串