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étodo | Descriçã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étodo | Descriçã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étodo | Descriçã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ário | Capacidade recomendada |
|---|---|
| Uso geral | 8 KB (padrão) |
| Arquivos grandes, leitura sequencial | 64 KB - 256 KB |
| Muitas escritas pequenas | 32 KB - 128 KB |
| Rede com latência alta | 16 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
- Módulo std::io — visão geral das traits e tipos de I/O
- Read e Write Traits — as traits fundamentais de I/O
- File e OpenOptions — abrir e criar arquivos
- Ler Arquivo em Rust — receita prática de leitura com BufReader
- Ler Arquivo CSV — processamento de CSV linha por linha
- Documentação oficial:
BufReader,BufWriter