Skip to content
Published at:

18. Input/Output — 输入输出

Rust 的 I/O 系统围绕三个核心 trait 组织:Read(字节输入)、Write(字节输出)和 BufRead(缓冲输入)。这些 trait 为文件、网络连接、内存缓冲区、管道、终端等提供了统一接口——一旦理解了这个模型,所有 I/O 操作都遵循相同的模式。本章从 trait 设计出发,覆盖文件操作、路径处理、目录操作和网络初探。

核心 Trait 体系

Rust I/O 的类型关系如下:

Read (字节读取)
├── File, TcpStream, Stdin, Cursor<&[u8]>
├── BufReader<R> (包装任意 Read 实现者为 BufRead)
├── &[u8] (内存字节数组)

BufRead (缓冲读取,继承 Read)
├── BufReader<R>
├── StdinLock
├── Cursor<&[u8]>

Write (字节写入)
├── File, TcpStream, Stdout, Stderr
├── Vec<u8> (数据追加到尾部)
├── BufWriter<W> (缓冲写入包装器)
├── Cursor<&mut [u8]>
└── Sink (丢弃所有写入)

导入 I/O 相关 trait 的惯用方式:

rust
use std::io::prelude::*;
use std::io::{self, Read, Write, ErrorKind};

Read Trait

Read 定义了从字节源读取数据的方法:

rust
pub trait Read {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;

    // 提供默认实现的方法
    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> { ... }
    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> { ... }
    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { ... }

    // 适配器方法
    fn bytes(self) -> Bytes<Self> { ... }
    fn chain<R: Read>(self, next: R) -> Chain<Self, R> { ... }
    fn take(self, limit: u64) -> Take<Self> { ... }
}

read:最底层方法

read 尝试读取一些字节到缓冲区,返回实际读取的字节数。返回 Ok(0) 表示输入结束:

rust
use std::io::Read;

fn read_exact_data(mut reader: impl Read) -> io::Result<Vec<u8>> {
    let mut buf = [0u8; 1024];
    let mut data = Vec::new();

    loop {
        match reader.read(&mut buf) {
            Ok(0) => break,            // EOF
            Ok(n) => data.extend_from_slice(&buf[..n]),
            Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        }
    }

    Ok(data)
}

关键行为:

  • read 可能读取少于请求的字节数——依赖返回值来确定实际读取量
  • ErrorKind::Interrupted 需要特殊处理——表示系统调用被信号中断,应重试
  • 返回 Ok(0)唯一的 EOF 信号

read_to_end 和 read_to_string

一次性读取所有内容:

rust
use std::io::Read;

// 读取全部字节到 Vec<u8>
let mut data = Vec::new();
reader.read_to_end(&mut data)?;

// 读取全部文本到 String(要求有效 UTF-8)
let mut text = String::new();
reader.read_to_string(&mut text)?;

// 安全注意:对不受信任的数据源使用 .take() 限制读取量
let safe_read = reader.take(10 * 1024 * 1024);  // 限制 10MB

read_exact

读取恰好填满缓冲区——与 read 不同,它保证要么读满要么以 UnexpectedEof 错误返回:

rust
let mut header = [0u8; 16];
reader.read_exact(&mut header)?;  // 不到 16 字节则出错

适配器方法

rust
// bytes()——返回逐字节迭代器
for byte_result in reader.bytes() {
    let byte = byte_result?;
    println!("{byte:#04x}");
}

// chain()——连接两个 reader
let combined = file1.chain(file2);

// take()——限制读取量
let limited = reader.take(1024);  // 最多读 1024 字节

BufRead Trait

BufRead 继承 Read,为缓冲 reader 添加按行读取等高级操作:

rust
pub trait BufRead: Read {
    fn read_line(&mut self, buf: &mut String) -> io::Result<usize>;
    fn lines(self) -> Lines<Self> { ... }
    fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> io::Result<usize>;
    fn split(self, byte: u8) -> Split<Self> { ... }
    fn fill_buf(&mut self) -> io::Result<&[u8]> { ... }
    fn consume(&mut self, amt: usize);
}

关键注意:File 没有实现 BufRead——必须用 BufReader::new(file) 包装才能按行读取。

read_line 和 lines

rust
use std::io::{BufRead, BufReader};
use std::fs::File;

let file = File::open("data.txt")?;
let reader = BufReader::new(file);

// read_line——读取一行(包含换行符)
let mut line = String::new();
reader.read_line(&mut line)?;
println!("第一行:{line}");  // 包含 '\n'

// lines()——迭代器,每行一个 String(不包含换行符)
for line_result in reader.lines() {
    let line = line_result?;
    println!("{line}");
}

lines() 是大多数文本处理任务的推荐方式:

rust
// grep 工具的简化实现
fn grep<R: BufRead>(target: &str, reader: R) -> io::Result<()> {
    for line_result in reader.lines() {
        let line = line_result?;
        if line.contains(target) {
            println!("{line}");
        }
    }
    Ok(())
}

// 从文件搜索
let file = File::open("log.txt")?;
grep("ERROR", BufReader::new(file))?;

// 从标准输入搜索
let stdin = io::stdin();
grep("ERROR", stdin.lock())?;

收集 lines 到 Vec

直接 collect() 会失败——需要利用 ResultFromIterator 实现:

rust
let file = File::open("data.txt")?;
let reader = BufReader::new(file);

// 这是错误的——类型不匹配
// let lines: Vec<String> = reader.lines().collect();  // 编译错误!

// 正确做法——利用 Result 的 FromIterator
let lines: Vec<String> = reader
    .lines()
    .collect::<io::Result<Vec<String>>>()?;

// 等价写法
let lines: Vec<String> = reader
    .lines()
    .filter_map(|r| r.ok())
    .collect();

Write Trait

Write 定义了向字节接收器写入数据的方法:

rust
pub trait Write {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize>;
    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { ... }
    fn flush(&mut self) -> io::Result<()>;

    // write_fmt——被 write! 宏调用
    fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> { ... }
}

write, write_all 和 flush

rust
use std::io::Write;

let mut file = File::create("output.txt")?;

// write——可能只写入部分数据
let bytes_written = file.write(b"Hello, ")?;

// write_all——确保全部写入
file.write_all(b"World!")?;

// flush——确保缓冲的数据到达目标
file.flush()?;

write! 和 writeln! 宏

print!/println! 不同的是,write!/writeln! 将输出定向到任意 writer(而非仅 stdout),并返回 io::Result

rust
use std::io::Write;

let mut file = File::create("report.txt")?;

writeln!(file, "Report Generated: {}", chrono::Local::now())?;
writeln!(file, "Name: {name}, Score: {score}", name = "Alice", score = 95)?;

// println! 在写入失败时会 panic
// println!("写入屏幕");
// writeln! 让你自己处理错误
// writeln!(io::stdout(), "写入 stdout")?;

BufWriter

为任意 writer 添加缓冲,减少系统调用:

rust
use std::io::{BufWriter, Write};

let file = File::create("large_output.txt")?;
let mut writer = BufWriter::new(file);

// 多次 write! 调用被缓冲
for i in 0..100_000 {
    writeln!(writer, "Line {i}")?;
}

// 重要:丢弃 BufWriter 时如果 flush 失败,错误会被静默吞掉
// 显式 flush 确保错误被处理
writer.flush()?;

BufWriter 默认缓冲区大小为 8 KB,可用 BufWriter::with_capacity(size, writer) 自定义。

Vec<u8> 作为 Writer

rust
use std::io::Write;

let mut buf = Vec::new();
write!(&mut buf, "Hello, {}!", "World")?;
writeln!(&mut buf)?;

assert_eq!(String::from_utf8(buf).unwrap(), "Hello, World!\n");

// 注意:String 没有实现 Write
// 需要先写入 Vec<u8> 再转换,或使用 fmt::Write 的 write!

std::io::copy

io::copy 是将数据从 reader 传输到 writer 的最高效方式——使用 8 KB 内部缓冲区:

rust
use std::io;

fn copy_file(src: &str, dst: &str) -> io::Result<u64> {
    let mut src_file = File::open(src)?;
    let mut dst_file = File::create(dst)?;
    // 返回复制的字节数
    io::copy(&mut src_file, &mut dst_file)
}

注意:io::copy 在 reader 返回 Ok(0) 之前不断循环,没有内置的大小限制——对不受信任的数据源应包装 .take(n)

File 类型

File 类型位于 std::fs 模块中,表示打开的文件:

rust
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};

// 打开已存在的文件(只读)
let mut file = File::open("input.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;

// 创建新文件(写入,存在则截断)
let mut file = File::create("output.txt")?;
file.write_all(b"Hello, World!")?;

// OpenOptions——精细控制打开模式
let file = OpenOptions::new()
    .read(true)
    .write(true)
    .create(true)      // 不存在则创建
    .append(true)      // 追加模式
    .open("log.txt")?;

let file = OpenOptions::new()
    .write(true)
    .create_new(true)  // 存在则返回错误
    .open("new_file.txt")?;

File 实现了 ReadWriteSeek。文件在 Drop 时自动关闭。

Seek Trait

Seek trait 支持在文件中重定位读写位置:

rust
use std::io::{Seek, SeekFrom};

let mut file = File::open("data.bin")?;

// 跳到文件开头
file.seek(SeekFrom::Start(0))?;

// 跳到第 100 字节
file.seek(SeekFrom::Start(100))?;

// 从末尾向前 8 字节
file.seek(SeekFrom::End(-8))?;

// 从当前位置偏移
file.seek(SeekFrom::Current(16))?;

// 获取当前位置
let pos = file.stream_position()?;

标准输入、输出和错误

stdinstdoutstderr 都持有互斥锁——每次读写都要获取锁:

rust
use std::io::{self, Read, Write, BufRead};

// 读取标准输入的一行
let mut line = String::new();
io::stdin().read_line(&mut line)?;

// 写入标准输出
io::stdout().write_all(b"Hello, World!\n")?;

// 写入标准错误
io::stderr().write_all(b"Error occurred!\n")?;

// 使用 lock() 获取独占锁——高效批量操作
let stdin = io::stdin();
let handle = stdin.lock();  // StdinLock 实现了 BufRead

for line_result in handle.lines() {
    let line = line_result?;
    println!("读取:{line}");
}
// lock 在 handle 离开作用域时自动释放

stdin() 返回的类型是 Stdin,不是 StdinLock。直接在 stdin() 上调用 .read_line() 隐式获取/释放锁——频繁调用效率低。lock() 获取一次锁然后可重复使用。

Path 和 PathBuf

Rust 使用 Path(类似 &str)和 PathBuf(类似 String)表示文件系统路径:

rust
use std::path::{Path, PathBuf};

// 创建
let path = Path::new("/usr/local/bin/rustc");
let path_buf = PathBuf::from("/home/user/document.txt");

// 组件访问
assert_eq!(path.parent(), Some(Path::new("/usr/local/bin")));
assert_eq!(path.file_name(), Some(OsStr::new("rustc")));
assert_eq!(path.extension(), Some(OsStr::new("")));  // rustc 无扩展名
assert_eq!(path.file_stem(), Some(OsStr::new("rustc")));

// 路径判断
let path = Path::new("/usr/local/bin");
assert!(path.is_absolute());
assert!(!path.is_relative());
assert!(path.is_dir());
assert!(!path.is_file());

// join——连接路径
let base = Path::new("/usr");
let full = base.join("local").join("bin");
assert_eq!(full, PathBuf::from("/usr/local/bin"));

// components——迭代路径组件
for component in path.components() {
    println!("{component:?}");
    // RootDir, Normal("usr"), Normal("local"), Normal("bin")
}

// ancestors——回溯到根的所有父目录
for ancestor in path.ancestors() {
    println!("{ancestor:?}");
    // /usr/local/bin
    // /usr/local
    // /usr
    // /
}

// 字符串转换
let path = Path::new("/usr/local/bin");
// to_str——可能返回 None(如果路径包含非 UTF-8 字节)
if let Some(s) = path.to_str() {
    println!("UTF-8: {s}");
}
// to_string_lossy——始终返回字符串(非法字节替换为 �)
println!("{}", path.to_string_lossy());
// display——返回 Display 实现
println!("{}", path.display());

类型对照表:

借用所有权核心能力
strStringUTF-8 文本处理
OsStrOsString任意字节序列,可能非 UTF-8
PathPathBuf路径操作:parent(), join(), components()

所有字符串和路径类型都实现了 AsRef<Path>——泛型函数可以声明为:

rust
fn open_config<P: AsRef<Path>>(path: P) -> io::Result<String> {
    std::fs::read_to_string(path)
}

// 接受多种类型
open_config("config.toml")?;
open_config(String::from("config.toml"))?;
open_config(PathBuf::from("config.toml"))?;
open_config(&path_buf)?;

目录操作

rust
use std::fs;
use std::path::Path;

// 创建目录
fs::create_dir("new_directory")?;           // 父目录必须存在
fs::create_dir_all("a/b/c/d")?;            // 递归创建所有父目录

// 删除目录
fs::remove_dir("empty_dir")?;               // 目录必须为空
fs::remove_dir_all("non_empty_dir")?;       // 递归删除(类似 rm -r)

// 读取目录内容
let entries = fs::read_dir("/home/user")?;

for entry in entries {
    let entry = entry?;
    let path = entry.path();
    let file_type = entry.file_type()?;

    let type_label = if file_type.is_dir() {
        "目录"
    } else if file_type.is_file() {
        "文件"
    } else if file_type.is_symlink() {
        "符号链接"
    } else {
        "其它"
    };

    println!("{}: {}", entry.file_name().to_string_lossy(), type_label);
}

注意:read_dir 不列出 ...

DirEntry 方法

rust
let entry: fs::DirEntry = ...;

entry.file_name();     // -> OsString(文件名)
entry.path();          // -> PathBuf(完整路径 = 目录路径 + 文件名)
entry.file_type();     // -> io::Result<FileType>
entry.metadata();      // -> io::Result<Metadata>(大小、权限、时间戳等)

常用文件系统函数

下表汇总了 std::fs 中最常用的函数:

函数功能Unix 等价Windows 等价
read_to_string(path)读取整个文件到 Stringcattype
read(path)读取整个文件到 Vec<u8>cattype
write(path, data)写入数据到文件(截断)echo > fileecho > file
copy(src, dst)复制文件cp -pCopyFileEx
rename(src, dst)重命名/移动文件mvMoveFileEx
remove_file(path)删除文件rmdel
hard_link(src, dst)创建硬链接lnCreateHardLink
canonicalize(path)规范化路径(解析符号链接)realpathGetFinalPathNameByHandle
metadata(path)获取文件元数据statGetFileInformationByHandle
symlink_metadata(path)获取元数据(不跟踪符号链接)lstat

递归拷贝目录示例

rust
use std::fs;
use std::path::Path;

fn copy_dir(src: &Path, dst: &Path) -> io::Result<()> {
    // 创建目标目录
    fs::create_dir(dst)?;

    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let target = dst.join(entry.file_name());

        if file_type.is_dir() {
            copy_dir(&entry.path(), &target)?;
        } else if file_type.is_file() {
            fs::copy(entry.path(), &target)?;
        }
        // 符号链接等特殊类型在此忽略
    }

    Ok(())
}

其他 Reader/Writer 类型

Cursor

内存中的读写器——对字节数组进行读写:

rust
use std::io::{Cursor, Read, Write, Seek, SeekFrom};

// 从字节切片读取
let data = b"Hello, World!";
let mut cursor = Cursor::new(data);
let mut buf = String::new();
cursor.read_to_string(&mut buf)?;
assert_eq!(buf, "Hello, World!");

// 向 Vec<u8> 写入
let mut cursor = Cursor::new(Vec::new());
write!(cursor, "Position: {}", 42)?;

// Seek 改变位置
cursor.seek(SeekFrom::Start(0))?;
let mut output = String::new();
cursor.read_to_string(&mut output)?;
println!("{output}");  // Position: 42

sink 和 empty

rust
use std::io::{self, Read, Write};

// sink()——黑洞 writer,丢弃所有数据
let mut sink = io::sink();
sink.write_all(b"这将被丢弃")?;

// empty()——空 reader,永远返回 EOF
let mut empty = io::empty();
let mut buf = [0; 10];
assert_eq!(empty.read(&mut buf)?, 0);

// repeat(byte)——无限重复同一个字节
let mut repeat = io::repeat(b'x');

网络 I/O 简介

std::net 模块提供 TCP 和 UDP 的跨平台支持:

TCP 客户端

rust
use std::io::{Read, Write};
use std::net::TcpStream;

let mut stream = TcpStream::connect(("example.com", 80))?;

// 发送 HTTP 请求
stream.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")?;

// 读取响应
let mut response = String::new();
stream.read_to_string(&mut response)?;
println!("{response}");

TCP Echo 服务器

rust
use std::io;
use std::net::{TcpListener, TcpStream};
use std::thread;

fn handle_client(mut stream: TcpStream) -> io::Result<()> {
    // 将接收到的数据原样写回
    io::copy(&mut stream, &mut stream.try_clone()?)?;
    Ok(())
}

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    println!("监听 127.0.0.1:8080");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| {
                    if let Err(e) = handle_client(stream) {
                        eprintln!("错误:{e}");
                    }
                });
            }
            Err(e) => eprintln!("连接失败:{e}"),
        }
    }

    Ok(())
}

TcpStream 同时实现了 ReadWrite——是 I/O trait 体系统一体性的最好体现。

注意:上面的 echo 服务器使用阻塞 I/O。对于高性能服务,应使用异步 I/O(tokioasync-std)——这将在异步编程章节中介绍。

错误处理

I/O 操作都返回 io::Result<T>Result<T, io::Error> 的类型别名)。关键错误信息:

rust
use std::io::{self, ErrorKind};

match File::open("config.toml") {
    Ok(file) => { /* 处理文件 */ }
    Err(e) => match e.kind() {
        ErrorKind::NotFound => eprintln!("配置文件未找到"),
        ErrorKind::PermissionDenied => eprintln!("没有读取权限"),
        ErrorKind::AlreadyExists => eprintln!("文件已存在"),
        ErrorKind::Interrupted => { /* 重试 */ }
        _ => eprintln!("其它 I/O 错误:{e}"),
    }
}

// 常见模式:传播错误
fn read_config() -> io::Result<String> {
    let content = std::fs::read_to_string("config.toml")?;
    Ok(content)
}

二进制数据、压缩和序列化

标准库的 I/O 只提供原始的字节读写。对于结构化数据,需要外部 crate:

byteorder crate(二进制读写)

rust
// Cargo.toml: byteorder = "1"
use std::io::Cursor;
use byteorder::{ReadBytesExt, WriteBytesExt, LittleEndian, BigEndian};

// 写入二进制数据
let mut buf = Vec::new();
buf.write_u32::<LittleEndian>(0x12345678)?;
buf.write_f64::<LittleEndian>(3.14159)?;

// 读取二进制数据
let mut reader = Cursor::new(buf);
let value: u32 = reader.read_u32::<LittleEndian>()?;
assert_eq!(value, 0x12345678);

serde crate(序列化框架)

rust
// Cargo.toml: serde = { version = "1", features = ["derive"] }
//            serde_json = "1"
use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    name: String,
    version: u32,
    features: Vec<String>,
}

// 序列化到 JSON
let config = Config {
    name: "MyApp".into(),
    version: 1,
    features: vec!["logging".into(), "caching".into()],
};

let json = serde_json::to_string(&config)?;
println!("{json}");

// 反序列化
let parsed: Config = serde_json::from_str(&json)?;
assert_eq!(parsed.name, "MyApp");

// 直接写入 writer
serde_json::to_writer(&mut std::io::stdout(), &config)?;

小结

  • Read/Write/BufRead 三个核心 trait 统一了所有 I/O 操作。任何实现了这些 trait 的类型(文件、网络连接、内存缓冲、管道、终端)都可以互换使用。
  • File 实现了 Read 和 Write 但没有实现 BufRead——用 BufReader::new(file) 包装获得按行读取能力。BufWriter 减少系统调用次数,显式 flush() 确保错误不丢失。
  • Path/PathBuf 处理文件系统路径,所有字符串类型都实现了 AsRef<Path>——泛型函数可以同时接受 &strString&PathPathBuf
  • 文件系统操作std::fs)提供了创建/删除/复制/重命名/遍历目录等完整功能。canonicalize() 解析符号链接和相对路径。
  • 网络 I/Ostd::net)中 TcpStream 同时实现了 ReadWriteTcpListener::incoming() 返回连接迭代器——与文件 I/O 使用完全相同的模式。