16 - 输入与输出

16.1 - 读取器与写入器

  • Rust 标准库针对输入与输出的特性,是通过 ReadBufReadWrite 特型,以及实现它们的各种类型创建的。
  • 实现 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::Readstd::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 的特型 ReadBufReadWriteSeek 的导入方法:
  • 导入一个专用的前置模块,可以直接包含它们:
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),其中 errio::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):返回一个新的读取器,包含 reader1reader2 的所有输入。
  • reader.take(n):从与 reader 相同的数据源读取输入,但只读取 n 字节,再返回一个新的读取器。
  • 读取器和写入去都会实现 Drop 特型,在操作完成后会自动关闭。

16.1.2 - 缓冲读取器

  • 缓冲:给读取器和写入器分配一块内存作为缓冲区,暂时保存输入和输出的数据。缓冲可以减少系统调用。
  • 缓冲读取器实现了 ReadBufRead 两个特型。
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))
  • FileBufReader 是两个不同的库特性,因为有时候需要不带缓冲的文件,也有时候需要非文件的缓冲。
  • 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 的项,收集到类型 Cwhere 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]&strVec<u8>
  • Cursor 内部只有 buf 本身和一个整数。该整数用来表示在 buf 中的偏移量,初始值为 0。
  • Cursor 实现了 ReadBufReadSeek 特型。
  • 如果 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 包:提供了 ReadBytesExtWriteBytesExt 特型,为所有二进制输入和输出的读取器和写入器提供方法。
  • flate2 包:为读、写 gzip 压缩的数据提供了额外的适配器方法。
  • serde 包:面向序列化和反序列化,可以实现 Rust 数据结构与字节之间的转换。
  • serde::Serialize 特型的 serialize 方法:为所有支持序列化的类型服务,如字符串、字符、元组、向量和 HashMap
  • serde 也支持派生特型,以服务于自定义类型:
#[derive(Serialize, Deserialize)]
struct Player {
  location: String,
  items: Vec<String>,
  health: u32
}