std::io
std::io 是 Rust 的输入输出模块, 提供了读取输入、写入输出、错误处理等基础能力
介绍
std::io 中比较重要的内容:
| 类型/接口 | 作用 |
|---|---|
| stdin | 标准输入(键盘) |
| stdout | 标准输出(屏幕) |
| stderr | 标准错误输出 |
| Read | 读取字节的 trait(文件/网络/缓冲区都实现了) |
| Write | 写入字节的 trait |
| BufReader | 带缓冲的读取器, 减少系统调用 |
| Error | IO 错误类型 |
读取用户输入
最常用的读取输入方式是 read_line, 它是 std::io::stdin() 返回的 Stdin 上的方法
rust
use std::io;
fn main() {
// read_line: 读取一行用户输入到字符串
// 注意: 返回的字符串会包含结尾的换行符 \n
let mut name = String::new();
io::stdin().read_line(&mut name).expect("读取失败");
println!("你好, {name}"); // 你好, 张三 (用户输入)
}经典场景: 模拟简单的命令行问答
rust
use std::io;
fn main() {
// 循环读取用户输入, 输入 exit 退出
loop {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取失败");
// trim: 去掉首尾空白(包括换行符)
let input = input.trim();
if input == "exit" {
println!("再见!");
break;
}
println!("你输入了: {input}");
}
}向标准输出写入
println! 宏已经足够常用, 但是有些场景需要直接使用 Write trait, 比如写入字节数据
rust
use std::io::Write;
fn main() {
// write_all: 写入字节数据(注意是 b"..." 字节字符串)
let mut stdout = std::io::stdout();
stdout.write_all(b"hello io\n").expect("写入失败");
stdout.flush().expect("刷新失败");
// eprintln 已经讲过, 这里演示通过 stderr 写入
let mut stderr = std::io::stderr();
stderr.write_all(b"error message\n").expect("写入失败");
}缓冲读取 BufReader
直接读取文件/网络数据时, 每读一次都会发生一次系统调用, 使用 BufReader 可以先读一大块到内存, 再按需取用
rust
use std::io::BufRead;
use std::io::BufReader;
fn main() {
// BufReader::new: 任意实现了 Read 的类型都可以包一层缓冲
let data = "第一行\n第二行\n第三行\n";
let mut reader = BufReader::new(data.as_bytes());
// read_line: 逐行读取(需要引入 BufRead trait)
let mut line = String::new();
reader.read_line(&mut line).expect("读取失败");
println!("第一行: {line}"); // 第一行: 第一行
// lines: 返回每行迭代器(已经去掉换行符)
let data = "a\nb\nc\n";
let reader = BufReader::new(data.as_bytes());
for line in reader.lines() {
let line = line.expect("读取失败");
println!("{line}"); // a / b / c
}
}std::fs
std::fs 是 Rust 的文件系统模块, 提供了读写文件、操作目录等能力
读取文件
| 方法 | 作用 |
|---|---|
| fs::read_to_string | 读取整个文件为字符串 |
| fs::read | 读取整个文件为字节数据 |
| File::open | 打开文件(只读), 再配合 Read trait 读取 |
rust
use std::fs;
use std::io::Read;
fn main() {
// 先创建文件(示例自包含, 写读闭环)
let path = std::env::temp_dir().join("rust_read_demo.txt");
fs::write(&path, "hello 文件系统\n第二行").expect("写入失败");
// 1. read_to_string: 读取整个文件为字符串
let content = fs::read_to_string(&path).expect("读取失败");
println!("{content}"); // hello 文件系统 / 第二行
// 2. read: 读取整个文件为字节数据
let bytes = fs::read(&path).expect("读取失败");
println!("bytes len = {}", bytes.len());
// 3. File::open + read_to_string: 分步读取
let mut file = fs::File::open(&path).expect("打开失败");
let mut content = String::new();
file.read_to_string(&mut content).expect("读取失败");
println!("{content}");
// 清理临时文件
fs::remove_file(&path).expect("删除失败");
}写入文件
| 方法 | 作用 |
|---|---|
| fs::write | 一步完成"创建+写入" |
| File::create | 创建文件, 再配合 Write trait 写入 |
rust
use std::fs;
use std::io::Write;
fn main() {
let path = std::env::temp_dir().join("rust_write_demo.txt");
// 1. fs::write: 一步完成创建+写入
fs::write(&path, "第一次写入").expect("写入失败");
println!("{}", fs::read_to_string(&path).expect("读取失败")); // 第一次写入
// 2. File::create + write_all: 分步写入
// 注意: b"..." 是字节字符串, 只能包含 ASCII 字符, 中文要用 .as_bytes()
let mut file = fs::File::create(&path).expect("创建失败");
file.write_all("第二次写入".as_bytes()).expect("写入失败");
println!("{}", fs::read_to_string(&path).expect("读取失败")); // 第二次写入
fs::remove_file(&path).expect("删除失败");
}注意点
fs::write 和 File::create 都是覆盖式写入: 文件已存在时会清空原内容, 需要追加内容请使用 OpenOptions
追加写入
使用 OpenOptions 可以灵活控制打开方式, 比如追加模式
rust
use std::fs::OpenOptions;
use std::io::Write;
fn main() {
let path = std::env::temp_dir().join("rust_append_demo.txt");
// append(true): 追加模式
// create(true): 文件不存在时自动创建
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(&path)
.expect("打开失败");
// 注意: b"..." 字节字符串只能包含 ASCII, 中文用 .as_bytes()
file.write_all("第1行\n".as_bytes()).expect("写入失败");
file.write_all("第2行\n".as_bytes()).expect("写入失败");
let content = std::fs::read_to_string(&path).expect("读取失败");
println!("{content}"); // 第1行 / 第2行
std::fs::remove_file(&path).expect("删除失败");
}目录操作
| 方法 | 作用 |
|---|---|
| create_dir | 创建目录(父目录不存在会报错) |
| create_dir_all | 递归创建目录(父目录不存在会自动创建) |
| read_dir | 遍历目录, 返回目录条目迭代器 |
| remove_dir | 删除空目录 |
| remove_dir_all | 递归删除目录(不管里面有没有内容) |
rust
use std::fs;
fn main() {
let base = std::env::temp_dir().join("rust_dir_demo");
// create_dir: 创建目录
fs::create_dir(&base).expect("创建目录失败");
println!("create_dir 成功");
// create_dir_all: 递归创建(中间目录不存在也会创建)
let nested = base.join("a/b/c");
fs::create_dir_all(&nested).expect("递归创建失败");
println!("create_dir_all 成功");
// 在目录里放一个文件, 用于演示 read_dir
fs::write(base.join("a/hello.txt"), "hi").expect("写入失败");
// read_dir: 遍历目录(条目顺序不确定)
for entry in fs::read_dir(base.join("a")).expect("读取目录失败") {
let entry = entry.expect("读取条目失败");
println!("{}", entry.file_name().to_string_lossy()); // b / hello.txt (顺序不确定)
}
// remove_dir: 只能删除空目录
fs::remove_dir(base.join("a/b/c")).expect("删除目录失败");
println!("remove_dir 成功");
// remove_dir_all: 递归删除(目录非空也可以删)
fs::remove_dir_all(&base).expect("递归删除失败");
println!("remove_dir_all 成功");
}文件操作
| 方法 | 作用 |
|---|---|
| remove_file | 删除文件 |
| rename | 重命名/移动文件 |
| copy | 复制文件, 返回复制的字节数 |
| exists | 判断路径是否存在 |
rust
use std::fs;
fn main() {
let base = std::env::temp_dir().join("rust_file_ops");
fs::create_dir_all(&base).expect("创建目录失败");
// 准备源文件
let src = base.join("src.txt");
fs::write(&src, "hello").expect("写入失败");
// copy: 复制文件, 返回复制的字节数
let dst = base.join("dst.txt");
let copied = fs::copy(&src, &dst).expect("复制失败");
println!("copied = {copied} 字节"); // copied = 5 字节
// rename: 重命名(也可以用来移动文件)
let renamed = base.join("renamed.txt");
fs::rename(&dst, &renamed).expect("重命名失败");
// exists: 判断路径是否存在
println!("renamed 存在 = {}", renamed.exists()); // renamed 存在 = true
println!("dst 存在 = {}", dst.exists()); // dst 存在 = false
// remove_file: 删除文件
fs::remove_file(&renamed).expect("删除失败");
println!("删除后存在 = {}", renamed.exists()); // 删除后存在 = false
fs::remove_dir_all(&base).expect("清理失败");
}文件信息
使用 metadata 获取文件的元信息
| 方法 | 作用 |
|---|---|
| metadata | 获取文件/目录的元信息 |
| len | 文件大小(字节) |
| is_file | 是否是文件 |
| is_dir | 是否是目录 |
rust
use std::fs;
fn main() {
let path = std::env::temp_dir().join("rust_meta_demo.txt");
fs::write(&path, "hello").expect("写入失败");
// metadata: 获取元信息
let meta = fs::metadata(&path).expect("获取元信息失败");
println!("len = {}", meta.len()); // len = 5
println!("is_file = {}", meta.is_file()); // is_file = true
println!("is_dir = {}", meta.is_dir()); // is_dir = false
println!("readonly = {}", meta.permissions().readonly()); // readonly = false
// 目录的元信息
let dir_meta = fs::metadata(std::env::temp_dir()).expect("获取元信息失败");
println!("temp_dir is_dir = {}", dir_meta.is_dir()); // temp_dir is_dir = true
fs::remove_file(&path).expect("删除失败");
}错误处理与 ? 运算符
文件操作几乎都会返回 Result, 使用 ? 运算符可以方便地把错误向上传播
rust
use std::fs;
fn main() {
let path = std::env::temp_dir().join("rust_question.txt");
fs::write(&path, "42").expect("写入失败");
// 读取成功
match read_number(&path) {
Ok(n) => println!("数字 = {n}"), // 数字 = 42
Err(e) => println!("出错: {e}"),
}
// 文件不存在, ? 会把错误传播给调用方
match read_number(&std::env::temp_dir().join("no_such_file.txt")) {
Ok(n) => println!("数字 = {n}"),
Err(e) => println!("出错: {e}"), // 出错: No such file or directory (os error 2)
}
fs::remove_file(&path).expect("删除失败");
}
// ? 运算符: 读取失败/解析失败都会自动返回 Err
fn read_number(path: &std::path::Path) -> Result<i32, std::io::Error> {
let content = fs::read_to_string(path)?;
// 注意: parse 返回的错误类型是 ParseIntError, 不是 io::Error
// 需要用 map_err 转换成 io::Error 才能用 ? 传播
let n = content
.trim()
.parse::<i32>()
.map_err(|_| std::io::Error::other("内容不是数字"))?;
Ok(n)
}main 函数也可以返回 Result, 出错时程序会打印错误并以非 0 状态码退出
rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 使用 Box<dyn Error> 统一错误类型
// 读取文件(io::Error) 和 解析数字(ParseIntError) 是不同的错误类型,
// 但它们都实现了 std::error::Error, 所以都可以直接用 ? 传播
let path = std::env::temp_dir().join("rust_box_demo.txt");
std::fs::write(&path, "42").expect("写入失败");
let num: i32 = std::fs::read_to_string(&path)?.trim().parse()?;
println!("num = {num}"); // num = 42
std::fs::remove_file(&path).expect("删除失败");
Ok(())
}经典场景: 统计文件行数
rust
use std::fs;
use std::io::BufRead;
fn main() {
let path = std::env::temp_dir().join("rust_lines_demo.txt");
fs::write(&path, "第一行\n第二行\n第三行\n").expect("写入失败");
// BufReader + lines: 逐行读取
let file = fs::File::open(&path).expect("打开失败");
let reader = std::io::BufReader::new(file);
let mut count = 0;
for line in reader.lines() {
let line = line.expect("读取失败");
println!("第{}行: {line}", count + 1); // 第1行: 第一行 / 第2行: 第二行 / 第3行: 第三行
count += 1;
}
println!("总共 {count} 行"); // 总共 3 行
fs::remove_file(&path).expect("删除失败");
}std::path
std::path 模块提供了跨平台的路径类型: Path 和 PathBuf
Path 与 PathBuf 的区别
Path 和 PathBuf 的关系, 和 &str 与 String 的关系一模一样:
| 类型 | 是否拥有数据 | 类似关系 |
|---|---|---|
| Path | 借用(不拥有) | 类似 &str |
| PathBuf | 拥有(可以修改) | 类似 String |
rust
use std::path::{Path, PathBuf};
fn main() {
// Path: 借用已有的路径字符串
let p: &Path = Path::new("/tmp/hello.txt");
println!("{p:?}"); // "/tmp/hello.txt"
// PathBuf: 拥有自己的数据, 可以修改
let mut pb: PathBuf = PathBuf::from("/tmp");
pb.push("hello.txt");
println!("{pb:?}"); // "/tmp/hello.txt"
// PathBuf 可以自动借用为 Path
let p2: &Path = &pb;
println!("{p2:?}"); // "/tmp/hello.txt"
}创建路径
| 方法 | 作用 |
|---|---|
| Path::new | 从字符串创建 Path(借用) |
| PathBuf::from | 从字符串创建 PathBuf(拥有) |
| push | 追加路径段(修改自身) |
| join | 拼接路径(不修改自身, 返回新路径) |
rust
use std::path::PathBuf;
fn main() {
// 1. PathBuf::from: 从字符串创建
let mut base = PathBuf::from("/home/user");
println!("{base:?}"); // "/home/user"
// 2. push: 追加路径段(会修改自身)
base.push("projects");
base.push("rust_demo");
println!("{base:?}"); // "/home/user/projects/rust_demo"
// 3. join: 不修改自身, 返回拼接后的新路径
let full = base.join("src").join("main.rs");
println!("{full:?}"); // "/home/user/projects/rust_demo/src/main.rs"
println!("{base:?}"); // "/home/user/projects/rust_demo" (base 没有变)
}路径常用方法
| 方法 | 作用 |
|---|---|
| file_name | 文件名(最后一段) |
| extension | 文件扩展名 |
| parent | 父路径 |
| is_absolute | 是否是绝对路径 |
| exists | 路径是否存在 |
rust
use std::path::Path;
fn main() {
let path = Path::new("/home/user/projects/main.rs");
println!("file_name = {:?}", path.file_name()); // file_name = Some("main.rs")
println!("extension = {:?}", path.extension()); // extension = Some("rs")
println!("parent = {:?}", path.parent()); // parent = Some("/home/user/projects")
println!("is_absolute = {}", path.is_absolute()); // is_absolute = true
let rel = Path::new("docs/readme.md");
println!("is_absolute = {}", rel.is_absolute()); // is_absolute = false
println!("starts_with docs = {}", rel.starts_with("docs")); // starts_with docs = true
// exists: 判断路径是否存在
println!("exists = {}", path.exists()); // exists = false
}经典场景: 遍历目录
rust
use std::fs;
use std::path::Path;
fn main() {
// 准备测试目录
let base = std::env::temp_dir().join("rust_walk_demo");
fs::create_dir_all(base.join("sub")).expect("创建失败");
fs::write(base.join("a.txt"), "a").expect("写入失败");
fs::write(base.join("sub/b.txt"), "b").expect("写入失败");
// 遍历一层目录, 区分文件和目录
println!("=== 第一层 ===");
for entry in fs::read_dir(&base).expect("读取目录失败") {
let entry = entry.expect("读取条目失败");
let path = entry.path();
if path.is_dir() {
println!("[目录] {}", path.file_name().unwrap().to_string_lossy());
} else {
println!("[文件] {}", path.file_name().unwrap().to_string_lossy());
}
}
// 递归遍历所有层
println!("=== 递归遍历 ===");
walk(&base);
fs::remove_dir_all(&base).expect("清理失败");
}
fn walk(dir: &Path) {
for entry in fs::read_dir(dir).expect("读取目录失败") {
let entry = entry.expect("读取条目失败");
let path = entry.path();
if path.is_dir() {
walk(&path); // 递归进入子目录
} else {
println!("{}", path.display());
}
}
}相关开源库
标准库的文件操作已经够用, 但以下库可以简化一些复杂场景:
rust
// Cargo.toml 添加依赖:
// [dependencies]
// walkdir = "2"
fn main() {
// walkdir: 一行代码递归遍历目录
for entry in walkdir::WalkDir::new(".")
.into_iter()
.filter_map(|e| e.ok())
{
println!("{}", entry.path().display());
}
}rust
// Cargo.toml 添加依赖:
// [dependencies]
// tempfile = "3"
fn main() {
// NamedTempFile: 创建临时文件, 程序结束时自动删除
let file = tempfile::NamedTempFile::new().expect("创建临时文件失败");
println!("临时文件: {}", file.path().display());
// 程序退出时, 临时文件会被自动清理
}