Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ввод/вывод

Стандартная библиотека Rust предоставляет модуль std::io, который содержит универсальные интерфейсы и типы для операций ввода/вывода.

В этом модуле находятся два базовых трэйта, задающих единый интерфейс для чтения и записи: Read и Write.

Read

Трэйт Read определяет один необходимый к реализации метод — read и несколько вспомогательных методов, работающих поверх него.

#![allow(unused)]
fn main() {
pub trait Read {
    fn read(&mut self, buf: &mut [u8]) -> Result;
    // ...
}
}

Вызов read заполняет переданные ему слайс байтами и возвращает количество байт, которые были записаны в слайс. Если размер слайса меньше, чем количество доступных байт, то метод read придётся вызвать несколько раз.

Остальные методы трэйта Read имеют реализацию по умолчанию. Некоторые из них:

  • fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize>
    Считывает всё содержимое в вектор байт.
  • fn read_to_string(&mut self, buf: &mut String) -> Result<usize>
    Считывает всё содержимое в в строку. Предполагается, что считываемые данные — текст.
  • fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> Result<()>
    Считывает содержимое в курсор (о них мы поговорим позже).

Трэйт Read реализован для целого ряда типов: файл, сетевой сокет, Unix канал, STDIN (стандартный ввод), т.д.


Давайте рассмотрим работу с трэйтом Read на примере коллекции VecDeque<u8>, которую мы изучили в прошлой главе и которая также реализует трэйт Read.

Note

Программисты на Java могут провести аналогию с чтением из массива байт при помощи класса java.io.ByteArrayInputStream.

use std::{collections::VecDeque, io::Read};

fn main() {
    // Создаём VecDeque, из которого будем читать посредством трэйта Read
    let mut v: VecDeque<u8> = VecDeque::from(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);

    // Буфер размером в 2 байта. В него мы будем производить чтение
    let mut buf: [u8; 2] = [0; 2];

    // Считываем первый байт
    let read_bytes: Result<usize, std::io::Error> = v.read(&mut buf);
    println!("Buffer: {buf:?}, Number of read bytes: {read_bytes:?}");

    // В цикле считываем оставшиеся байты
    while let Ok(read_bytes) = v.read(&mut buf) && read_bytes > 0 {
        println!("Buffer: {buf:?}, Number of read bytes: {read_bytes:?}");
    }
}

Вывод программы:

Buffer: [1, 2], Number of read bytes: Ok(2)
Buffer: [3, 4], Number of read bytes: 2
Buffer: [5, 6], Number of read bytes: 2
Buffer: [7, 8], Number of read bytes: 2
Buffer: [9, 8], Number of read bytes: 1

Если источник данных небольшой (все данные можно легко считать в оперативную память), то удобнее воспользоваться методом read_to_end:

use std::{collections::VecDeque, io::Read};

fn main() {
    let mut v = VecDeque::from(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);

    let mut buf: Vec<u8> = Vec::new();
    let read_bytes: Result<usize, std::io::Error> = v.read_to_end(&mut buf);
    println!("Buffer: {buf:?}, Number of read bytes: {read_bytes:?}");
    // Buffer: [1, 2, 3, 4, 5, 6, 7, 8, 9], Number of read bytes: Ok(9)
}

В случае если считываемые данные — текст, то подойдёт метод read_to_string:

use std::{collections::VecDeque, io::Read};

fn main() {
    // 65 - 'A', 66 - 'B', 67 - 'C', 68 - 'D', 69 - 'E'
    let mut v = VecDeque::from(vec![65, 66, 67, 68, 69]);

    let mut buf = String::new();
    let read_bytes: Result<usize, std::io::Error> = v.read_to_string(&mut buf);
    println!("Buffer: {buf}, Number of read bytes: {read_bytes:?}");
    // Buffer: ABCDE, Number of read bytes: Ok(5)
}

Cursor

Коллекция VecDeque<u8> реализует трэйт Read, но коллекция Vec<u8> — нет. Так было сделано потому, что VecDeque позволяет эффективно извлекать элементы из начала коллекции (элементы, считанные из VecDeque посредством read, удаляются), а из Vec извлекать элементы эффективно можно только с конца. Однако если необходимо иметь возможность работать с Vec<u8> как с Read, то это можно сделать при помощи обёртки std::io::Cursor.

Cursor имеет два поля: оборачиваемая коллекция и текущая позиция курсора.

#![allow(unused)]
fn main() {
pub struct Cursor<T> {
    inner: T,
    pos: u64,
}
}

Эта позиция позволяет эффективно реализовать операцию чтения: вместо того, чтобы удалять вычитанные элементы из вектора, курсор будет просто “сдвигать” позицию к еще не считанным байтам. Именно таким образом и реализован трэйт Read для Cursor.

#![allow(unused)]
fn main() {
impl<T> Read for Cursor<T> where T: AsRef<[u8]> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let n = Read::read(&mut Cursor::split(self).1, buf)?;
        self.pos += n as u64;
        Ok(n)
    }
}
}

Как видим, Read реализован для любой вариации Cursor, которая хранит в себе любой тип, реализующий AsRef<u8>. Тип Vec<u8> как раз реализует AsRef<u8>.

use std::{io::Read, io::Cursor};

fn main() {
    let v: Vec<u8> = vec![65, 66, 67, 68, 69];
    let mut c: Cursor<Vec<u8>> = Cursor::new(v);
    let mut buf = String::new();
    let read_bytes: Result<usize, std::io::Error> = c.read_to_string(&mut buf);
    println!("String: {buf}, Number of read bytes: {read_bytes:?}");
}

Вывод программы:

String: ABCDE, Number of read bytes: Ok(5)

Write

Трэйт Write определяет универсальный интерфейс для записи данных.

В трэйте объявлены два обязательных к реализации метода и еще несколько методов с реализацией по умолчанию.

#![allow(unused)]
fn main() {
pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;
    // ...
}
}

Метод write записывает все байты из буфера-аргумента и возвращает количество фактически записанных байт. В конкретной реализации допускается записывать только часть байт, если эта часть может быть записана быстро, а запись оставшихся байт займёт существенно больше времени.

Метод flush имеет смысл для типов, которые перед фактической записью данных, предварительно буферизируют их в памяти. Вызов метода должен приводить к сбросу буферов в конечное хранилище данных.

Также трэйт содержит метод с реализацией по умолчанию — write_all.

#![allow(unused)]
fn main() {
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
}

Этот метод, в отличие от write, должен записывать все байты, даже если это займёт существенно больше времени.

Как и Read, трэйт Write реализован для файла, сетевого сокета, Unix канала, STDOUT, т.д.


В отличие от Read, трэйт Write реализован не только для VecDeque<u8>, но и для Vec<u8>, так как вектор позволяет эффективно добавлять элементы в свой конец.

Рассмотрим пример записи байт в Vec<u8> посредством трэйта Write:

use std::io::Write;

fn main() {
    let mut v: Vec<u8> = vec![1, 2, 3, 4, 5];
    let count = v.write(&[6, 7, 8, 9]);
    println!("Written: {count:?}, vec: {v:?}");
    // Written: Ok(4), vec: [1, 2, 3, 4, 5, 6, 7, 8, 9]
}

Стандартный вывод STDOUT также позволяет работать с собой через трэйт Write:

use std::io::Write;

fn main() {
    let mut stdout = std::io::stdout();
    let _ = stdout.write(&[65, 66, 67, 68, 69, 10]);
}

Программа напечатает:

ABCDE

В следующей главе мы рассмотрим работу с Read и Write на примере файловой системы.