Как я могу создать эффективный итератор символов из стандартного ввода с Rust? - PullRequest
0 голосов
/ 17 мая 2018

Теперь, когда Read::chars итератор официально объявлен устаревшим , каков правильный способ получить итератор для символов, поступающих из Reader типа stdin, без чтениявесь поток в память?

Ответы [ 2 ]

0 голосов
/ 21 мая 2018

Как уже упоминали другие, можно скопировать устаревшую реализацию Read::chars для использования в вашем собственном коде. Является ли это действительно идеальным или нет, будет зависеть от вашего варианта использования - для меня это оказалось достаточно хорошим на данный момент, хотя вполне вероятно, что мое приложение перерастет этот подход в ближайшем будущем.

Чтобы проиллюстрировать, как это можно сделать, давайте рассмотрим конкретный пример:

use std::io::{self, Error, ErrorKind, Read};
use std::result;
use std::str;

struct MyReader<R> {
    inner: R,
}

impl<R: Read> MyReader<R> {
    fn new(inner: R) -> MyReader<R> {
        MyReader {
            inner,
        }
    }

#[derive(Debug)]
enum MyReaderError {
    NotUtf8,
    Other(Error),
}

impl<R: Read> Iterator for MyReader<R> {
    type Item = result::Result<char, MyReaderError>;

    fn next(&mut self) -> Option<result::Result<char, MyReaderError>> {
        let first_byte = match read_one_byte(&mut self.inner)? {
            Ok(b) => b,
            Err(e) => return Some(Err(MyReaderError::Other(e))),
        };
        let width = utf8_char_width(first_byte);
        if width == 1 {
            return Some(Ok(first_byte as char));
        }
        if width == 0 {
            return Some(Err(MyReaderError::NotUtf8));
        }
        let mut buf = [first_byte, 0, 0, 0];
        {
            let mut start = 1;
            while start < width {
                match self.inner.read(&mut buf[start..width]) {
                    Ok(0) => return Some(Err(MyReaderError::NotUtf8)),
                    Ok(n) => start += n,
                    Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
                    Err(e) => return Some(Err(MyReaderError::Other(e))),
                }
            }
        }
        Some(match str::from_utf8(&buf[..width]).ok() {
            Some(s) => Ok(s.chars().next().unwrap());
            None => Err(MyReaderError::NotUtf8),
        })
    }
}

Приведенный выше код также требует реализации read_one_byte и utf8_char_width. Они должны выглядеть примерно так:

static UTF8_CHAR_WIDTH: [u8; 256] = [
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x1F
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x3F
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x5F
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // 0x7F
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0x9F
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 0xBF
0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // 0xDF
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, // 0xEF
4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0, // 0xFF
];

fn utf8_char_width(b: u8) -> usize {
    return UTF8_CHAR_WIDTH[b as usize] as usize;
}

fn read_one_byte(reader: &mut Read) -> Option<io::Result<u8>> {
    let mut buf = [0];
    loop {
        return match reader.read(&mut buf) {
            Ok(0) => None,
            Ok(..) => Some(Ok(buf[0])),
            Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
            Err(e) => Some(Err(e)),
        };
    }
}

Теперь мы можем использовать реализацию MyReader для создания итератора char s для некоторого читателя, например io::stdin::Stdin:

fn main() {
    let stdin = io::stdin();
    let mut reader = MyReader::new(stdin.lock());
    for c in reader {
        println!("{}", c);
    }
}

Ограничения этого подхода подробно обсуждаются в оригинальной ветке выпуска . Одна особая проблема , на которую стоит обратить внимание, заключается в том, что этот итератор не будет правильно обрабатывать потоки, не кодированные в UTF-8.

0 голосов
/ 17 мая 2018

Соответствующая проблема для устаревания хорошо суммирует проблемы с Read::chars и предлагает предложения:

Код, который не заботится о пошаговой обработке данных, может использовать Read::read_to_stringвместо.Код, который заботится, по-видимому, также хочет контролировать свою стратегию буферизации и работать с &[u8] и &str срезами, которые являются максимально большими, а не одним char за один раз.Он должен быть основан на функции str::from_utf8, а также на методах valid_up_to и error_len типа Utf8Error.Один хитрый аспект касается случаев, когда один char представлен в UTF-8 несколькими байтами, где эти байты оказываются разделенными на отдельные части вызовов / 1018 * вызовов / буферов.(Utf8Error::error_len возврат None указывает, что это может иметь место.) utf-8 ящик решает эту проблему, но для обеспечения гибкости предоставляет API, который, вероятно, имеет слишком большую поверхность для включения вСтандартная библиотека.

Конечно, выше для данных, которые всегда UTF-8.Если необходимо поддерживать другую кодировку символов, попробуйте использовать encoding_rs или encoding ящик.

Ваш собственный итератор

наиболее эффективное решение с точки зрения количества вызовов ввода / вывода - это чтение всего в гигантский буфер String и итерация по нему:

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

fn main() {
    let stdin = io::stdin();
    let mut s = String::new();
    stdin.lock().read_to_string(&mut s).expect("Couldn't read");
    for c in s.chars() {
        println!(">{}<", c);
    }
}

Вы можете объединить это с ответом от Есть ли собственная версия String :: chars? :

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

fn reader_chars<R: Read>(mut rdr: R) -> io::Result<impl Iterator<Item = char>> {
    let mut s = String::new();
    rdr.read_to_string(&mut s)?;
    Ok(s.into_chars()) // from https://stackoverflow.com/q/47193584/155423
}

fn main() -> io::Result<()> {
    let stdin = io::stdin();

    for c in reader_chars(stdin.lock())? {
        println!(">{}<", c);
    }

    Ok(())
}

Теперь у нас есть функция, которая возвращает итератор char сдля любого типа, который реализует Read.

Как только у вас есть этот шаблон, нужно просто решить, где сделать компромисс между распределением памяти и запросами ввода-вывода.Вот аналогичная идея, в которой используются буферы размера строки:

use std::io::{BufRead, BufReader, Read};

fn reader_chars<R: Read>(rdr: R) -> impl Iterator<Item = char> {
    // We use 6 bytes here to force emoji to be segmented for demo purposes
    // Pick more appropriate size for your case
    let reader = BufReader::with_capacity(6, rdr);

    reader
        .lines()
        .flat_map(|l| l) // Ignoring any errors
        .flat_map(|s| s.into_chars())  // from https://stackoverflow.com/q/47193584/155423
}

fn main() {
    // emoji are 4 bytes each
    let data = "????";
    let data = data.as_bytes();

    for c in reader_chars(data) {
        println!(">{}<", c);
    }
}

Дальнейшая крайняя задача - выполнить один запрос ввода-вывода для каждого символа.Это не займет много памяти, но потребует много ресурсов ввода-вывода.

Прагматичный ответ

Скопируйте и вставьте реализацию Read::chars в свой собственный код.Он будет работать так же, как раньше.

См. Также:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...