BufReader e BufWriter em Rust

Referência completa de BufReader e BufWriter em Rust: I/O bufferizado, lines(), read_line(), with_capacity e impacto na performance.

BufReader e BufWriter em Rust

BufReader e BufWriter são wrappers que adicionam buffering a qualquer tipo que implemente Read ou Write, respectivamente. Sem buffering, cada chamada a read() ou write() resulta em uma syscall ao sistema operacional — o que é extremamente custoso quando feito milhares de vezes com poucos bytes. Com buffering, as operações são agrupadas em blocos maiores, reduzindo drasticamente o número de syscalls.

Visão geral e tipos-chave

BufReader

BufReader<R> envolve qualquer R: Read e mantém um buffer interno (padrão: 8 KB). Além de implementar Read, implementa BufRead, que fornece métodos como lines() e read_line().

BufWriter

BufWriter<W> envolve qualquer W: Write e acumula escritas em um buffer interno antes de enviá-las ao destino. O buffer é automaticamente descarregado quando está cheio ou quando flush() é chamado.

Trait BufRead

A trait BufRead estende Read com acesso ao buffer interno e métodos orientados a linhas:

pub trait BufRead: Read {
    fn fill_buf(&mut self) -> io::Result<&[u8]>;
    fn consume(&mut self, amt: usize);
    fn read_line(&mut self, buf: &mut String) -> io::Result<usize>;
    fn lines(self) -> Lines<Self>;
    fn split(self, byte: u8) -> Split<Self>;
    fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> io::Result<usize>;
}

Padrões comuns com código

Leitura linha por linha com BufReader

O padrão mais comum é ler arquivos linha por linha usando lines():

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

fn main() -> io::Result<()> {
    let arquivo = File::open("log.txt")?;
    let leitor = BufReader::new(arquivo);

    for (num, linha) in leitor.lines().enumerate() {
        let linha = linha?; // cada linha pode falhar independentemente
        println!("{:6}: {}", num + 1, linha);
    }

    Ok(())
}

Leitura interativa com read_line

Para leitura incremental (ex: protocolo de rede), use read_line():

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

fn processar_cabecalhos(caminho: &str) -> io::Result<Vec<(String, String)>> {
    let arquivo = File::open(caminho)?;
    let mut leitor = BufReader::new(arquivo);
    let mut cabecalhos = Vec::new();
    let mut linha = String::new();

    loop {
        linha.clear(); // IMPORTANTE: reutilizar o buffer
        let bytes = leitor.read_line(&mut linha)?;

        if bytes == 0 || linha.trim().is_empty() {
            break; // EOF ou linha vazia = fim dos cabeçalhos
        }

        if let Some((chave, valor)) = linha.trim().split_once(':') {
            cabecalhos.push((
                chave.trim().to_string(),
                valor.trim().to_string(),
            ));
        }
    }

    Ok(cabecalhos)
}

fn main() -> io::Result<()> {
    let cabecalhos = processar_cabecalhos("requisicao.txt")?;
    for (chave, valor) in &cabecalhos {
        println!("{}: {}", chave, valor);
    }
    Ok(())
}

Escrita eficiente com BufWriter

use std::fs::File;
use std::io::{self, BufWriter, Write};

fn gerar_relatorio(caminho: &str, registros: &[(String, f64)]) -> io::Result<()> {
    let arquivo = File::create(caminho)?;
    let mut escritor = BufWriter::new(arquivo);

    writeln!(escritor, "RELATÓRIO DE VENDAS")?;
    writeln!(escritor, "====================")?;
    writeln!(escritor, "{:<30} {:>12}", "Produto", "Valor (R$)")?;
    writeln!(escritor, "{}", "-".repeat(44))?;

    let mut total = 0.0;
    for (produto, valor) in registros {
        writeln!(escritor, "{:<30} {:>12.2}", produto, valor)?;
        total += valor;
    }

    writeln!(escritor, "{}", "-".repeat(44))?;
    writeln!(escritor, "{:<30} {:>12.2}", "TOTAL", total)?;

    // flush() explícito garante que tudo foi escrito
    escritor.flush()?;
    Ok(())
}

fn main() -> io::Result<()> {
    let vendas = vec![
        ("Notebook Dell".to_string(), 4599.90),
        ("Mouse Logitech".to_string(), 189.50),
        ("Teclado Mecânico".to_string(), 349.00),
        ("Monitor 27 polegadas".to_string(), 1899.00),
    ];
    gerar_relatorio("relatorio.txt", &vendas)?;
    println!("Relatório gerado com sucesso");
    Ok(())
}

Tabela de métodos e funções

BufReader

MétodoDescrição
BufReader::new(reader)Cria com buffer padrão de 8 KB
BufReader::with_capacity(cap, reader)Cria com capacidade customizada
buffer()Retorna referência ao conteúdo do buffer interno
capacity()Retorna a capacidade total do buffer
into_inner()Consome o BufReader e retorna o leitor interno
get_ref() / get_mut()Acesso ao leitor interno por referência
seek_relative(offset)Busca relativa sem descartar o buffer

BufWriter

MétodoDescrição
BufWriter::new(writer)Cria com buffer padrão de 8 KB
BufWriter::with_capacity(cap, writer)Cria com capacidade customizada
buffer()Retorna referência ao conteúdo não descarregado
capacity()Retorna a capacidade total do buffer
into_inner()Descarrega e retorna o escritor interno
get_ref() / get_mut()Acesso ao escritor interno por referência
flush()Envia dados bufferizados ao destino

BufRead (trait)

MétodoDescrição
lines()Iterador sobre linhas (remove \n e \r\n)
read_line(&mut buf)Lê uma linha (inclui \n) no buffer
split(byte)Iterador que divide por um byte delimitador
read_until(byte, &mut buf)Lê até encontrar o byte delimitador
fill_buf()Preenche e retorna o buffer interno
consume(n)Marca N bytes como consumidos no buffer

Exemplos práticos

Exemplo 1: Processar arquivo CSV grande linha por linha

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

fn analisar_csv(caminho: &str) -> io::Result<()> {
    let arquivo = File::open(caminho)?;
    // Buffer maior para arquivos grandes (64 KB)
    let leitor = BufReader::with_capacity(64 * 1024, arquivo);

    let mut totais_por_categoria: HashMap<String, f64> = HashMap::new();
    let mut linhas_processadas = 0u64;

    for (num, resultado) in leitor.lines().enumerate() {
        let linha = resultado?;

        // Pular cabeçalho
        if num == 0 {
            continue;
        }

        let campos: Vec<&str> = linha.split(',').collect();
        if campos.len() >= 3 {
            let categoria = campos[1].trim().to_string();
            if let Ok(valor) = campos[2].trim().parse::<f64>() {
                *totais_por_categoria.entry(categoria).or_insert(0.0) += valor;
            }
        }
        linhas_processadas += 1;
    }

    println!("Processadas {} linhas", linhas_processadas);
    for (cat, total) in &totais_por_categoria {
        println!("  {}: R$ {:.2}", cat, total);
    }

    Ok(())
}

fn main() {
    if let Err(e) = analisar_csv("vendas.csv") {
        eprintln!("Erro ao processar CSV: {}", e);
    }
}

Exemplo 2: Gerar arquivo grande eficientemente com BufWriter

use std::fs::File;
use std::io::{self, BufWriter, Write};

fn gerar_dados_teste(caminho: &str, num_linhas: usize) -> io::Result<()> {
    let arquivo = File::create(caminho)?;
    // Buffer de 128 KB para escrita intensiva
    let mut escritor = BufWriter::with_capacity(128 * 1024, arquivo);

    writeln!(escritor, "id,nome,valor,ativo")?;

    for i in 0..num_linhas {
        writeln!(
            escritor,
            "{},{},{},.{}",
            i,
            format!("item_{:06}", i),
            (i as f64 * 1.5),
            if i % 3 == 0 { "true" } else { "false" }
        )?;
    }

    // into_inner() faz flush automaticamente e retorna o File
    let arquivo_inner = escritor.into_inner().map_err(|e| e.into_error())?;
    let metadata = arquivo_inner.metadata()?;
    println!(
        "Arquivo gerado: {} bytes ({} linhas)",
        metadata.len(),
        num_linhas
    );

    Ok(())
}

fn main() -> io::Result<()> {
    gerar_dados_teste("dados_teste.csv", 1_000_000)?;
    Ok(())
}

Exemplo 3: Dividir entrada por delimitador customizado

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

fn processar_registros_delimitados(dados: &[u8], delimitador: u8) -> io::Result<Vec<String>> {
    let leitor = io::BufReader::new(dados);
    let mut registros = Vec::new();

    for parte in leitor.split(delimitador) {
        let bytes = parte?;
        let texto = String::from_utf8_lossy(&bytes).to_string();
        if !texto.trim().is_empty() {
            registros.push(texto.trim().to_string());
        }
    }

    Ok(registros)
}

fn main() -> io::Result<()> {
    // Registros separados por '\0' (null byte)
    let dados = b"registro1\0registro2\0registro3\0";
    let registros = processar_registros_delimitados(dados, b'\0')?;
    for (i, reg) in registros.iter().enumerate() {
        println!("[{}] {}", i, reg);
    }

    // Registros separados por '|' (pipe)
    let dados = b"campo_a|campo_b|campo_c|";
    let campos = processar_registros_delimitados(dados, b'|')?;
    println!("Campos: {:?}", campos);

    Ok(())
}

Exemplo 4: Buffer duplo — BufReader + BufWriter para transformação

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

fn converter_formato(
    entrada: &str,
    saida: &str,
    transformar: fn(&str) -> String,
) -> io::Result<(u64, u64)> {
    let leitor = BufReader::new(File::open(entrada)?);
    let mut escritor = BufWriter::new(File::create(saida)?);

    let mut linhas_lidas = 0u64;
    let mut linhas_escritas = 0u64;

    for resultado in leitor.lines() {
        let linha = resultado?;
        linhas_lidas += 1;

        let convertida = transformar(&linha);
        if !convertida.is_empty() {
            writeln!(escritor, "{}", convertida)?;
            linhas_escritas += 1;
        }
    }

    escritor.flush()?;
    Ok((linhas_lidas, linhas_escritas))
}

fn main() -> io::Result<()> {
    // Exemplo: remover linhas em branco e converter para maiúsculas
    let (lidas, escritas) = converter_formato(
        "entrada.txt",
        "saida.txt",
        |linha| {
            let trimmed = linha.trim();
            if trimmed.is_empty() {
                String::new()
            } else {
                trimmed.to_uppercase()
            }
        },
    )?;

    println!("Lidas: {}, Escritas: {}", lidas, escritas);
    Ok(())
}

Quando usar buffering e impacto na performance

Cenário sem buffering (lento)

use std::fs::File;
use std::io::{self, Write};
use std::time::Instant;

fn sem_buffering() -> io::Result<()> {
    let mut arquivo = File::create("/tmp/sem_buffer.txt")?;
    let inicio = Instant::now();

    // Cada write() = uma syscall ao SO
    for i in 0..100_000 {
        writeln!(arquivo, "Linha {}: dados de exemplo", i)?;
    }

    println!("Sem buffer: {:?}", inicio.elapsed());
    Ok(())
}

Cenário com buffering (rápido)

use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::time::Instant;

fn com_buffering() -> io::Result<()> {
    let arquivo = File::create("/tmp/com_buffer.txt")?;
    let mut escritor = BufWriter::new(arquivo);
    let inicio = Instant::now();

    // Escritas acumuladas em memória, poucas syscalls
    for i in 0..100_000 {
        writeln!(escritor, "Linha {}: dados de exemplo", i)?;
    }
    escritor.flush()?;

    println!("Com buffer: {:?}", inicio.elapsed());
    Ok(())
}

Em testes típicos, a versão com BufWriter é 5x a 20x mais rápida que a versão sem buffering.

Escolhendo a capacidade do buffer

CenárioCapacidade recomendada
Uso geral8 KB (padrão)
Arquivos grandes, leitura sequencial64 KB - 256 KB
Muitas escritas pequenas32 KB - 128 KB
Rede com latência alta16 KB - 64 KB
Arquivos pequenos (< 4 KB)Não usar buffering
use std::io::BufReader;
use std::fs::File;

// Buffer customizado de 256 KB para arquivo grande
let arquivo = File::open("arquivo_grande.dat")?;
let leitor = BufReader::with_capacity(256 * 1024, arquivo);

Padrões de tratamento de erro para I/O

Cuidado com into_inner() do BufWriter

Ao chamar into_inner(), se o flush falhar, o BufWriter retorna um IntoInnerError que ainda contém o escritor:

use std::fs::File;
use std::io::{self, BufWriter, Write};

fn salvar_com_verificacao(caminho: &str, dados: &str) -> io::Result<()> {
    let arquivo = File::create(caminho)?;
    let mut escritor = BufWriter::new(arquivo);
    escritor.write_all(dados.as_bytes())?;

    match escritor.into_inner() {
        Ok(_arquivo) => {
            println!("Dados salvos com sucesso");
            Ok(())
        }
        Err(erro) => {
            eprintln!("Erro ao descarregar buffer: {}", erro.error());
            // erro.into_inner() recupera o BufWriter para tentar novamente
            Err(erro.into_error())
        }
    }
}

BufWriter faz flush no Drop, mas ignora erros

Quando um BufWriter sai de escopo, ele tenta fazer flush dos dados restantes. Porém, se o flush falhar, o erro é silenciosamente ignorado. Sempre chame flush() ou into_inner() explicitamente em código de produção.

Veja também