【Rust 笔记】16-输入与输出(上)

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
      }
      

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十八章
原文地址

猜你喜欢

转载自blog.csdn.net/feiyanaffection/article/details/125575331