I/O: lendo e escrevendo arquivos
Nesta aula, você aprenderá a realizar operações de leitura e escrita de arquivos em Rust usando a biblioteca padrão. Exploraremos os módulos std::fs, as traits Read e Write, os buffers BufReader e BufWriter, e as melhores práticas para tratamento de erros, com exemplos práticos e exercícios.
Nesta aula, vamos mergulhar no mundo da entrada e saída (I/O) de arquivos em Rust. A capacidade de ler e escrever arquivos é fundamental para quase qualquer programa, seja para persistir dados, carregar configurações ou processar informações. Rust oferece uma biblioteca padrão robusta, com módulos e traits que tornam essas operações seguras e eficientes, alinhadas à filosofia da linguagem de segurança sem sacrificar performance.
Vamos explorar desde os conceitos básicos de std::fs até técnicas mais avançadas com buffers, sempre com atenção ao tratamento de erros, que é um dos pilares da confiabilidade em Rust. Ao final, você terá uma base sólida para implementar I/O de arquivos em seus próprios projetos.
std::fs
O módulo std::fs fornece funcionalidades para manipular o sistema de arquivos: criar, ler, escrever, renomear e excluir arquivos e diretórios. É a porta de entrada para operações de I/O com arquivos. As funções principais são read_to_string, write, File::create, File::open, entre outras.
Um ponto importante é que Rust adota uma abordagem baseada em Result para operações que podem falhar, o que força o programador a lidar explicitamente com erros. Isso contrasta com linguagens que usam exceções e ajuda a evitar falhas silenciosas.
use std::fs;
use std::io::Result;
fn main() -> Result<()> {
// Lendo um arquivo inteiro para uma String
let conteudo = fs::read_to_string("exemplo.txt")?;
println!("Conteúdo: {}", conteudo);
// Escrevendo em um arquivo (sobrescreve se existir)
fs::write("saida.txt", "Olá, mundo!")?;
Ok(())
}No exemplo, usamos fs::read_to_string para ler todo o conteúdo de um arquivo e fs::write para gravar dados. O operador ? propaga erros automaticamente para a função, que retorna um Result. Isso simplifica o código e mantém a clareza.
Para operações mais finas, como abrir um arquivo com opções específicas, usamos File::open (somente leitura) e File::create (escrita, truncando se existir). Essas funções retornam um File que implementa as traits Read e Write, que veremos a seguir.
Read e Write
As traits std::io::Read e std::io::Write são abstrações fundamentais para I/O. Elas definem métodos como read, read_to_string, write e write_all. Qualquer tipo que implemente essas traits pode ser usado de forma genérica, permitindo que funções operem sobre arquivos, sockets, buffers de memória, etc.
Essas traits são essenciais para criar código reutilizável. Por exemplo, uma função que processa dados de qualquer fonte de leitura pode aceitar um parâmetro do tipo impl Read, sem se preocupar se é um arquivo ou um buffer.
use std::io::{Read, Write};
use std::fs::File;
fn main() -> std::io::Result<()> {
// Abre um arquivo para leitura
let mut arquivo = File::open("dados.bin")?;
let mut buffer = [0u8; 1024];
// Lê até 1024 bytes
let bytes_lidos = arquivo.read(&mut buffer)?;
println!("Li {} bytes", bytes_lidos);
// Abre um arquivo para escrita (cria ou sobrescreve)
let mut saida = File::create("saida.bin")?;
saida.write_all(&buffer[..bytes_lidos])?;
Ok(())
}No código acima, read preenche o buffer e retorna o número de bytes lidos, que pode ser menor que o tamanho do buffer. Já write_all garante que todos os dados do slice sejam escritos, lidando com escritas parciais internamente.
Outro método útil é read_to_end, que lê todos os bytes até o fim do arquivo e os coloca em um Vec<u8>. Isso é conveniente para arquivos binários.
BufReader/BufWriter
Operações de I/O são relativamente lentas em comparação com o acesso à memória. Para reduzir o número de chamadas ao sistema, Rust oferece std::io::BufReader e std::io::BufWriter. Esses tipos adicionam uma camada de buffer em memória, lendo ou escrevendo em blocos maiores, o que melhora significativamente a performance.
BufReader envolve um leitor e fornece métodos como read_line e lines, que são muito convenientes para processar texto linha por linha. BufWriter acumula escritas em um buffer e só envia para o destino quando o buffer está cheio ou quando é explicitamente chamado flush.
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
fn main() -> std::io::Result<()> {
// Lendo um arquivo linha por linha
let arquivo = File::open("texto.txt")?;
let leitor = BufReader::new(arquivo);
for linha in leitor.lines() {
let linha = linha?;
println!("{}", linha);
}
// Escrevendo com buffer
let arquivo_saida = File::create("saida.txt")?;
let mut escritor = BufWriter::new(arquivo_saida);
writeln!(escritor, "Linha 1")?;
writeln!(escritor, "Linha 2")?;
escritor.flush()?;
Ok(())
}No exemplo, BufReader::lines retorna um iterador sobre as linhas do arquivo, tratando automaticamente as quebras de linha. Já BufWriter acumula as escritas e o flush força a gravação no disco. Sem o flush, o buffer seria esvaziado quando o escritor fosse dropado (no final do escopo), mas é uma boa prática chamar explicitamente em pontos críticos.
É importante lembrar que BufReader e BufWriter são genéricos e podem ser usados com qualquer tipo que implemente Read ou Write, como sockets TCP, por exemplo.
Tratamento de erros
O tratamento de erros em Rust é explícito e encoraja a lidar com falhas de forma robusta. As operações de I/O retornam Result<T, std::io::Error>, onde std::io::Error contém informações sobre o que falhou. A forma mais comum de propagar erros é usar o operador ?, que retorna o erro da função atual se ocorrer uma falha.
Além de propagar, você pode capturar e tratar erros de várias maneiras: usando match, if let, ou os métodos unwrap e expect (que causam pânico se o erro ocorrer). Para programas simples, unwrap é aceitável, mas em código de produção é melhor lidar com os erros de forma graciosa.
use std::fs::File;
use std::io::{self, Read};
fn ler_arquivo(caminho: &str) -> Result<String, io::Error> {
let mut arquivo = File::open(caminho)?;
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo)?;
Ok(conteudo)
}
fn main() {
match ler_arquivo("config.toml") {
Ok(conteudo) => println!("Config: {}", conteudo),
Err(e) => eprintln!("Erro ao ler arquivo: {}", e),
}
}
Neste exemplo, ler_arquivo propaga erros com ?, e o chamador usa match para tratar o sucesso ou a falha. Em aplicações maiores, você pode definir seus próprios tipos de erro e usar crates como thiserror ou anyhow para simplificar a conversão de erros.
Uma boa prática é evitar o uso de unwrap em código que será executado em produção, pois um pânico pode derrubar o programa. Prefira sempre retornar Result e tratar os erros no nível adequado.
Boas práticas e observações finais
Ao trabalhar com arquivos, lembre-se de que os recursos são liberados automaticamente quando o objeto File sai de escopo, graças ao drop. Isso significa que você não precisa fechar explicitamente, mas se estiver usando BufWriter, é aconselhável chamar flush para garantir que todos os dados sejam gravados antes de o buffer ser descartado.
Para arquivos grandes, evite ler todo o conteúdo de uma vez; use BufReader e processe em partes. Isso economiza memória e melhora a performance.
Além disso, considere usar caminhos relativos ao diretório do projeto ou variáveis de ambiente para localizar arquivos, tornando seu programa mais portátil.
Exercícios
- Exercício 1: Escreva um programa que leia um arquivo chamado
entrada.txte imprima o número de linhas que ele contém. UtilizeBufReadere o métodolines(). - Exercício 2: Crie um programa que copie um arquivo binário de origem para destino usando
std::fs::copye depois faça uma cópia manual usandoReadeWritecom um buffer de 1024 bytes. - Exercício 3: Escreva uma função que receba um caminho de arquivo e retorne um
Result<Vec<u8>, std::io::Error>, lendo todos os bytes do arquivo. Use essa função para ler um arquivo e imprimir o número de bytes. - Exercício 4: Crie um programa que escreva um texto em um arquivo usando
BufWritere depois o leia de volta usandoBufReader, verificando se o conteúdo é igual. Imprima uma mensagem de sucesso ou erro. - Exercício 5: Implemente uma função que receba um nome de arquivo e retorne um
Result<String, std::io::Error>. Se o arquivo não existir, a função deve retornar um erro personalizado com a mensagem "Arquivo não encontrado". UseFile::opene trate o erro commap_err.
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> {
let arquivo = File::open("entrada.txt")?;
let leitor = BufReader::new(arquivo);
let mut contador = 0;
for _linha in leitor.lines() {
contador += 1;
}
println!("Número de linhas: {}", contador);
Ok(())
}
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
fn main() -> std::io::Result<()> {
// Cópia simples
fs::copy("origem.bin", "destino1.bin")?;
// Cópia manual
let mut origem = File::open("origem.bin")?;
let mut destino = File::create("destino2.bin")?;
let mut buffer = [0u8; 1024];
loop {
let n = origem.read(&mut buffer)?;
if n == 0 { break; }
destino.write_all(&buffer[..n])?;
}
Ok(())
}
use std::fs::File;
use std::io::{self, Read};
fn ler_bytes(caminho: &str) -> Result<Vec<u8>, io::Error> {
let mut arquivo = File::open(caminho)?;
let mut bytes = Vec::new();
arquivo.read_to_end(&mut bytes)?;
Ok(bytes)
}
fn main() -> Result<(), io::Error> {
let bytes = ler_bytes("dados.bin")?;
println!("O arquivo tem {} bytes", bytes.len());
Ok(())
}
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
fn main() -> std::io::Result<()> {
let texto = "Olá, mundo!\nEsta é uma linha.\n";
// Escrever
{
let arquivo = File::create("teste.txt")?;
let mut escritor = BufWriter::new(arquivo);
escritor.write_all(texto.as_bytes())?;
escritor.flush()?;
}
// Ler
let arquivo = File::open("teste.txt")?;
let leitor = BufReader::new(arquivo);
let mut lido = String::new();
for linha in leitor.lines() {
lido.push_str(&linha?);
lido.push('\n');
}
if lido == texto {
println!("Conteúdo coincidente!");
} else {
println!("Erro: o conteúdo não é igual.");
}
Ok(())
}
use std::fs::File;
use std::io::{self, Read};
fn ler_arquivo(caminho: &str) -> Result<String, io::Error> {
let mut arquivo = File::open(caminho)
.map_err(|_| io::Error::new(io::ErrorKind::NotFound, "Arquivo não encontrado"))?;
let mut conteudo = String::new();
arquivo.read_to_string(&mut conteudo)?;
Ok(conteudo)
}
fn main() {
match ler_arquivo("inexistente.txt") {
Ok(c) => println!("{}", c),
Err(e) => println!("Erro: {}", e),
}
}