Чтение файла по символу в Rust

Есть ли идиоматический способ обработки файла по одному символу за раз в Rust?

Кажется, это примерно то, что мне нужно:

let mut f = io::BufReader::new(try!(fs::File::open("input.txt")));

for c in f.chars() {
    println!("Character: {}", c.unwrap());
}

Но Read::chars по-прежнему нестабилен по сравнению с Rust v1.6.0.

Я рассмотрел использование Read::read_to_string, но файл может быть большим, и я не хочу читать его все в памяти.

Ответ 1

Давайте сравним 4 подхода.

1. Read::chars

Вы могли бы скопировать Read::chars, но он нестабилен с

семантика частичного чтения/записи, где происходят ошибки, в настоящее время неясна и может изменить

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

2. flat_map

Альтернатива flat_map не компилируется:

use std::io::{BufRead, BufReader};
use std::fs::File;

pub fn main() {
    let mut f = BufReader::new(File::open("input.txt").expect("open failed"));

    for c in f.lines().flat_map(|l| l.expect("lines failed").chars()) {
        println!("Character: {}", c);
    }
}

Проблема заключается в том, что chars берет из строки, но l.expect("lines failed") живет только внутри замыкания, поэтому компилятор дает ошибку borrowed value does not live long enough.

3. Вложенный для

Этот код

use std::io::{BufRead, BufReader};
use std::fs::File;

pub fn main() {
    let mut f = BufReader::new(File::open("input.txt").expect("open failed"));

    for line in f.lines() {
        for c in line.expect("lines failed").chars() {
            println!("Character: {}", c);
        }
    }
}

работает, но он сохраняет выделение строки для каждой строки. Кроме того, если в входном файле нет разрыва строки, весь файл будет загружен в память.

4. BufRead::read_until

Эффективной альтернативой памяти для подхода 3 является использование Read::read_until и использование одной строки для чтения каждой строки:

use std::io::{BufRead, BufReader};
use std::fs::File;

pub fn main() {
    let mut f = BufReader::new(File::open("input.txt").expect("open failed"));

    let mut buf = Vec::<u8>::new();
    while f.read_until(b'\n', &mut buf).expect("read_until failed") != 0 {
        // this moves the ownership of the read data to s
        // there is no allocation
        let s = String::from_utf8(buf).expect("from_utf8 failed");
        for c in s.chars() {
            println!("Character: {}", c);
        }
        // this returns the ownership of the read data to buf
        // there is no allocation
        buf = s.into_bytes();
        buf.clear();
    }
}

Ответ 2

Здесь есть два решения.

Сначала вы можете скопировать реализацию Read::chars() и использовать ее; что сделало бы совершенно тривиальным, чтобы перевести ваш код на стандартную реализацию библиотеки, если/когда он стабилизируется.

С другой стороны, вы можете просто перебирать строки за строкой (используя f.lines()), а затем использовать line.chars() в каждой строке, чтобы получить символы. Это немного более взломанно, но это определенно будет работать.

Если вам нужен только один цикл, вы можете использовать flat_map() с лямбдой, например |line| line.chars().