16 - 输入与输出
16.1 - 读取器与写入器
- Rust 标准库针对输入与输出的特性,是通过
Read
、BufRead
和Write
特型,以及实现它们的各种类型创建的。
- 实现
Read
的值是读取器(reader),有读取字节输入的方法。 - 实现
BufRead
的值是缓冲读取器,支持Read
的所有方法,且额外又支持读取文本行等的方法。 - 实现
Write
的值是写入器(writer),既支持字节输出,也支持 UTF-8 文本输出。
- 常用的读取器:读取字节
-
std::fs::File::open(filename)
:用于打开文件。 -
std::net::TcpStream
:用于从网络接收数据。 -
std::io::stdin()
:用于从进程的标准输入流读取数据。 -
std::io::Cursor<&[u8]>
值:从内存的字节数组中 “读取” 数据。
- 常用的写入器:写入字节
-
std::fs::File::create(filename)
:用于打开文件。 -
std::net::TcpStream
:用于通过网络发送数据。 -
std::io::stdout()
和std::io::stderr()
:用于将数据写入终端。 -
std::io::Cursor<&mut [u8]>
:允许将任何可修改字节切片作为文件写入。 -
Vec<u8>
:也是一个写入器,它的write
方法可以为向量追加元素。
- 基于
std::io::Read
和std::io::Write
特型实现的泛型代码,可以涵盖各种输入和输出渠道。
// 从任何读取器,将全部字节复制到任何写入器
use std::io::{self, Read, Write, ErrorKind};
const DEFAULT_BUF_SIZE: usize = 8 * 1024;
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W)
-> io::Result<u64>
where R: Read, W: Write
{
let mut buf = [0; DEFAULT_BUF_SIZE];
let mut written = 0'
loop {
let len = match reader.ead(&mut buf) {
Ok(0) => return Ok(written),
Ok(len) => len,
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
writer.write_all(&buf[..len])?;
written += len as u64;
}
}
-
std::io::copy()
是泛型的,可以将数据从File
复制到TcpStream
,从Stdin
复制到内存中的Vec<u8>
。
- 四个常用的
std::io
的特型Read
、BufRead
、Write
和Seek
的导入方法:
- 导入一个专用的前置模块,可以直接包含它们:
use std::io::prelude::*;
- 导入
std::io
模块自身
use std::io::{self, Read, Write, ErrorKind};
// self可以将io生命为`std::io`模块的别名,这样std::io::Result和std::io::Error,就可以简单地写成io::Result和io::Error。
16.1.1 - 读取器
std::io::Read
的常用读取器方法,用于读取数据,它们都以读取器本身的mut
引用作为参数:
reader.read(&mut buffer)
:从数据源读取某些字节,然后存储到给定的buffer
中。
buffer
参数的类型是&mut [u8]
。- 这个方法会读取
buffer.len()
个字节。 - 返回值的类型是
io::Result<u64>
,这个是Result<y64, io::Error>
类型的别名。
读取成功,会返回u64
值的读取字节数,数值<= buffer.len()
。
读取错误,.read()
返回Err(err)
,其中err
是io::Error
值。.kind()
方法可以返回io::ErrorKind
类型的错误码。
reader.read_to_end(&mut byte_vec)
:从读取器中读取出所有剩余输入,并追加到byte_vec
中。
-
byte_vec
是一个Vec<u8>
类型的值。 - 此方法返回
io::Result<(usize)>
,表示读取到的字节数。 - 此方法对添加到向量中的数据量没有限制,不要对不可信的数据源使用它。可以使用
.take()
方法来提高安全限制。
reader.read_to_string(&mut string)
:从读取器中读取出所有剩余输入,并追加到string
中。
- 如果输入流不是有效的 UTF-8,那么此方法会返回
ErrorKind::InvaliData
错误。 - 除 UTF-8 外的其他字符集,可以通过开源的
encoding
包支持。
read.read_exact(&mut buf)
:从读取器中读取恰好足够的数据,填充到给定的buffer
中。
- 参数类型是
&[u8]
。 - 如果读取器在读到
buf.len()
字节前就已经把数据读完了,那么此方法会返回ErrorKind::UnexpectedEof
错误。
std::io::Read
的常用适配器方法,以读取器(reader)的值作为参数,将其转换为一个迭代器或一个不同的读取器:
reader.bytes()
:返回输入流字节的迭代器。
- 迭代器项的类型是
io::Result<u8>
,每个字节都需要错误检查。 - 此方法对每个字节都会调用一次
reader.read()
,对没有缓冲的读取器来说效率很低。
reader.chars()
:读取器为 UTF-8,并返回迭代器项是字符的迭代器。无效的 UTF-8 会导致InvalidData
错误。reader1.chain(reader2)
:返回一个新的读取器,包含reader1
和reader2
的所有输入。reader.take(n)
:从与reader
相同的数据源读取输入,但只读取n
字节,再返回一个新的读取器。
- 读取器和写入去都会实现
Drop
特型,在操作完成后会自动关闭。
16.1.2 - 缓冲读取器
- 缓冲:给读取器和写入器分配一块内存作为缓冲区,暂时保存输入和输出的数据。缓冲可以减少系统调用。
- 缓冲读取器实现了
Read
和BufRead
两个特型。
BufRead
特型的常用读取器方法:
reader.read_line(&mut line)
:读取一行文本并追加到
line
。
-
line
是一个String
类型的值。 - 行尾的换行符
'\n'
或"\r\n"
也会包含在line
中。 - 返回值是
io::Result<usize>
,表示读取的字节数,包括行终结符。 - 如果读取处于输入末尾,则
line
不变,且返回Ok(0)
。
reader.lines()
:返回输入行的迭代器。
- 迭代项类型是
io::Result<String>
。 - 换行符不会包含在字符串中。
reader.read_until(stop_byte, &mut byte_vec)
和reader.split(stop_byte)
:与.read_line()
和.lines()
类似。但以字节为单位,产生Vec<u8>
值。stop_byte
表示界定符。.fill_buf()
和.consume(n)
:可以用于直接访问读取器内部的缓冲区。
16.1.3 - 读取文本行
- Unix 的
grep
命令分析:
- 搜索多行文本,并与管道组合使用,以查找指定写入器。
use std::io;
use std::io::prelude:: *;
fn grep(target: &str) -> io::Result<()> {
let stdin = io::stdin();
for line_result in stdin.lock().lines() {
let line = line_result?;
if line.contains(target) {
println!("{}", line);
}
}
Ok(())
}
- 进一步扩展,增加搜索磁盘上文件的功能,改进为泛型函数:
fn grep<R>(target: &str, reader: R) -> io::Result<()> where R: BufRead {
for line_result in reader.lines() {
let ine = line_result?;
if line.contains(target) {
println!("{}", line);
}
}
Ok(())
}
- 通过
StdinLock
或缓冲File
调用。
let stdin = io::stdin()
grep(&target, stdin.lock())?;
let f = File::open(file)?;
grep(&target, BufReader::new(f))
File
和BufReader
是两个不同的库特性,因为有时候需要不带缓冲的文件,也有时候需要非文件的缓冲。
-
File
不会自动缓冲,而是要通过BufReader::new(reader)
创建。 - 如果要设置缓冲区的到校,则可以使用
BufReader::with_capacity(size, reader)
。
- Unix 的
grep
命令完整程序:
// grep: 搜索stdin或某些文件中匹配指定字符串的行
use std::error::Error;
use std::io::{self, BufReader};
use std::io::prelude:: *;
use std::fs::File;
use std::path::PathBuf;
fn grep<R>(target: &str, reader: R) -> io::Result<()> where R: BufRead {
for line_result in reader.lines() {
let line = line_result?;
if line.contains(target) {
println!("{}", line);
}
}
Ok(())
}
fn grep_main() -> Result<(), Box<Error>> {
// 取得命令行参数。第一个参数是要搜索的字符串,其余参数是文件名
let mut args = std::env::args().skip(1);
let target = match args.next() {
Some(s) => s,
None = Err("usage: grep PATTERN FILE...")?
};
let files: Vec<PathBuf> = args.map(PathBuf::from).collect();
if files.is_empty() {
let stdin = io::stdin();
grep(&target, stdin.local())?;
} else {
for file in files {
let f = File::open(file)?;
grep(&target, BufReader::new(f))?;
}
}
Ok(())
}
fn main() {
let result = grep_main();
if let Err(err) = result {
let _ = writelen!(io::stderr(), "{}, err");
}
}
16.1.4 - 收集行
- 读取器方法会返回
Result
值的迭代器。 -
.collect()
可以实现收集行的操作。
let lines = reader.lines().collect::<io::Result<Vec<String>>>()?;
// io::Result<Vec<String>>是一个集合类型,因此.collect()方法可以创建并填充该类型的值。
- 标准库为
Result
实现了FromIterator
特型:
impl<T, E, C> FromIterator<Result<T, E>> for Result<C, E> where C: FromIterator<T> {
...
}
- 如果可以将类型
T
的项,收集到类型C
(where C: FromIterator<T>
)的集合中,那么就可以将类型Result<T, E>
的项收集为类型Result<C, E>(FromIterator<Result<T, E>> for Result<C, E>)
。
16.1.5 - 写入器
- 向标准输出流输出,可以使用
println!()
和print!()
宏。它们写入失败时只会诧异。 - 向写入器输出,则可以使用
writeln!()
和write!()
宏。
- 它们包含两个参数,第一个参数是写入器。
- 它们的返回值是
Result
。在使用时,建议以?
操作符结尾,用于处理错误。
Write
特型的方法:
-
writer.write(&buf)
:将切片buf
中的某些字节写入底层流。返回io::Result<usize>
,成功时包含写入的字节数,可能小于buf.len()
。该方法安全限制较低,尽量不要直接使用。 -
writer.write_all(&buf)
:将切片buf
中的所有字节都写入,返回Result<()>
。 -
writer.flush()
:将所有缓冲数据都写到底层流,返回Result<()>
。
- 与读取器类似,写入器也会在被清除时自动关闭。所有剩余缓冲数据会被写入底层写入器,在写入期间发生错误,错误会被忽略。为确保应用可以发现所有输出错误,应该在清除之前,手工使用
.flush()
方法清理缓冲写入器。 BufWriter::new(writer)
可以给任何写入器添加缓冲。BufReader::new(reader)
可一个任何读取器添加缓冲。
let file = File::create("tmp.txt")?;
let writer = BufWriter::new(file);
- 要设置读取器的缓冲区大小,可以使用
BufWriter::with_capacity(size, writer)
。
16.1.6 - 文件
- 打开文件的方式:
File::open(filename)
:打开已有文件供读取。返回一个io::Result<File>
,如果文件不存在会返回错误。File::create(finename)
:创建新文件供写入。如果指定名字的文件已存在,则该文件会删节。- 使用
OpenOptions
指定打开文件的行为
use std::fs::OpenOptions;
let log = OpenOptions::new()
.append(true) // 如果文件存在,则在末尾追加内容
.open("server.lgo")?;
let file = OpenOptions::new()
.write(true)
.create_new(true) // 如果文件存在则失败
.open("new_file.txt")?;
.append()
、.write()
、.create_new()
等都可以连缀调用,因为它们都返回 self
。这种方法连缀调用的模式,在 Rust 种被称为构建器(builder)。
File
类型在文件系统模块std::fs
中。File
打开后,可以与其他读取器或写入器一样使用。可以根据需要添加缓冲。File
也会在被清除时自动关闭。
16.1.7 - 搜寻
File
实现了 Seek
特型:支持在文件里跳转读取,而不是只能从头到尾一次性读取或写入。
pub trait Seek {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64>;
}
pub enum SeekFrom {
Start(u64),
End(i64),
Current(i64)
}
-
file.seek(SeekFrom::Start(0))
表示跳到开始位置。 -
file.seek(SeekFrom::Current(-8))
表示后退 8 字节。 - 无论是机械硬盘还是 SSD 固态硬盘,一次搜寻只能读取几兆数据。
16.1.8 - 其他读取器和写入器类型
io::stdin()
:返回标准输入流的读取器,类型为io::Stdin
。
- 它由所有线程共享,每次读取都设计获得和释放互斥量。
Stdin
的.lock()
方法,用于获得互斥量,并返回一个io::StdinLock
缓冲读取器,在被清除前会持有互斥量,避免互斥量开销。- 而
io::stdin().lock()
不能应用与互斥量,因为它会保存对Stdin
值的引用,要求Stdin
值必须保存在某个生命期较长的地方。但它可以用在收集行中。
let stdin = io::stdin();
let lines = stdin.lock().lines();
io::stdout()
:返回标准输出流的写入器。拥有互斥量和.lock()
方法。io::stderr()
:返回标准错误流的写入器。拥有互斥量和.lock()
方法。Vec<u8>
实现了Write
。
- 可以写入
Vec<u8>
,用新数据扩展向量。 -
String
没有实现Write
。要使用Write
构建字符串,首先需要写到一个Vec<u8>
中,然后再使用String::from_utf8(vec)
把限量转换为字符串。
Cursor::new(buf)
:创建一个新Cursor
,它是一个从buf
中读取数据的缓冲读取器。
- 用于创建读取
String
的读取器。 - 参数
buf
可以是实现AsRef<[u8]>
的任何类型,因此也可以传入&[u8]
、&str
或Vec<u8>
。 -
Cursor
内部只有buf
本身和一个整数。该整数用来表示在buf
中的偏移量,初始值为 0。 -
Cursor
实现了Read
、BufRead
和Seek
特型。 - 如果
buf
的类型是&mut [u8]
或Vec<u8>
,那么也支持Write
特型。Cursor<&mut [u8]>
和Cursor<Vec<u8>>
也实现了std::io::prelude
的所有 4 个特型。
std::net::TcpStream
:表示 TCP 网络连接。
- 既是读取器,也是写入器,以支持 TCP 双向通信。
-
TcpStream::connect(("hostname", PORT))
静态方法:尝试连接服务器,返回io::Result<TcpStream>
。
std::process::Command
:支持创建一个子进程,将数据导入其标准输入。
use std::process::{Command, Stdio};
let mut child = Command::new("grep")
.arg("-e")
.arg("a.*e.*i.*o.*u")
.stdin(Stdio::piped())
.spawn()?;
let mut to_child = child.stdin.take().unwrap();
for word in my_words {
writelen!(to_child, "{}", word)?;
}
drop(to_child); // 关闭grep的stdin
child.wait()?;
-
child.stdin
的类型是Option<std::process::ChildStdin>
。 -
Command
也有.stdout()
和.stderr()
方法。
std::io
模块:提供了一些函数,以返回简单的读取器和写入器。
-
io::sink()
:无操作写入器。所有写入方法都返回 Ok,但数据会被丢弃。 -
io::empty()
:无操作读取器。读取始终成功,但返回输入终止。 -
io::repeat(byte)
:返回的读取器会反复给出指定字节。
16.1.9 - 二进制数据、压缩和序列化 —— 开源包的 std::io
扩展
byteorder
包:提供了ReadBytesExt
和WriteBytesExt
特型,为所有二进制输入和输出的读取器和写入器提供方法。flate2
包:为读、写gzip
压缩的数据提供了额外的适配器方法。serde
包:面向序列化和反序列化,可以实现 Rust 数据结构与字节之间的转换。
serde::Serialize
特型的serialize
方法:为所有支持序列化的类型服务,如字符串、字符、元组、向量和HashMap
。serde
也支持派生特型,以服务于自定义类型:
#[derive(Serialize, Deserialize)]
struct Player {
location: String,
items: Vec<String>,
health: u32
}