Теперь, когда итератор Read::chars
официально объявлен устаревшим, как правильно получить итератор над символами, поступающими из Reader
, подобного стандартному вводу, без чтения всего потока в память?
Как я могу создать эффективный итератор символов из стандартного ввода с помощью Rust?
Ответы (2)
Соответствующий вопрос об устаревании хорошо суммирует проблемы с Read::chars
и предлагает предложения:
Код, который не заботится об инкрементальной обработке данных, может вместо этого использовать
Read::read_to_string
. Код, который заботится об этом, вероятно, также хочет управлять своей стратегией буферизации и работать с максимально большими&[u8]
и&str
слайсами, а не с однимchar
за раз. Он должен быть основан на функцииstr::from_utf8
, а также на методахvalid_up_to
иerror_len
Utf8Error
тип. Один сложный аспект связан со случаями, когда одинchar
представлен в UTF-8 несколькими байтами, где эти байты оказываются разделенными на отдельные вызовыread
/фрагменты буфера. (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
s для любого типа, реализующего 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
в свой собственный код. Он будет работать так же хорошо, как и раньше.
Смотрите также:
Как упомянули еще несколько человек, можно скопировать устаревшая реализация 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.