Read e Write Traits em Rust
As traits std::io::Read e std::io::Write são as abstrações centrais para I/O no Rust. Qualquer fonte de dados implementa Read (arquivos, sockets, buffers em memória), e qualquer destino implementa Write. Programar contra essas traits torna seu código genérico — a mesma função pode processar dados de um arquivo, da rede ou de um vetor de bytes.
Visão geral e tipos-chave
Trait Read
A trait Read define a interface para leitura de bytes. Seu único método obrigatório é read(), mas ela fornece vários métodos auxiliares com implementações padrão.
pub trait Read {
// Método obrigatório
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
// Métodos com implementação padrão
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize>;
fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize>;
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()>;
fn chain<R: Read>(self, next: R) -> Chain<Self, R>;
fn take(self, limit: u64) -> Take<Self>;
fn bytes(self) -> Bytes<Self>;
// ...
}
Trait Write
A trait Write define a interface para escrita de bytes. Possui dois métodos obrigatórios: write() e flush().
pub trait Write {
// Métodos obrigatórios
fn write(&mut self, buf: &[u8]) -> io::Result<usize>;
fn flush(&mut self) -> io::Result<()>;
// Métodos com implementação padrão
fn write_all(&mut self, buf: &[u8]) -> io::Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()>;
// ...
}
Padrões comuns com código
Leitura básica com Read
use std::fs::File;
use std::io::{self, Read};
fn main() -> io::Result<()> {
let mut arquivo = File::open("dados.txt")?;
// read() — lê até encher o buffer, retorna quantos bytes leu
let mut buffer = [0u8; 512];
let n = arquivo.read(&mut buffer)?;
println!("Lidos {} bytes: {:?}", n, &buffer[..n]);
// read_to_string() — lê tudo como texto UTF-8
let mut arquivo = File::open("texto.txt")?;
let mut conteudo = String::new();
let tamanho = arquivo.read_to_string(&mut conteudo)?;
println!("Lidos {} bytes: {}", tamanho, &conteudo[..50.min(conteudo.len())]);
// read_to_end() — lê tudo como bytes
let mut arquivo = File::open("imagem.png")?;
let mut bytes = Vec::new();
arquivo.read_to_end(&mut bytes)?;
println!("Arquivo tem {} bytes", bytes.len());
Ok(())
}
Leitura exata com read_exact
O read_exact() garante que o buffer inteiro seja preenchido, ou retorna erro:
use std::fs::File;
use std::io::{self, Read};
fn ler_cabecalho(caminho: &str) -> io::Result<[u8; 8]> {
let mut arquivo = File::open(caminho)?;
let mut cabecalho = [0u8; 8];
// Garante que exatamente 8 bytes sejam lidos
// Retorna UnexpectedEof se o arquivo for menor
arquivo.read_exact(&mut cabecalho)?;
Ok(cabecalho)
}
fn main() -> io::Result<()> {
match ler_cabecalho("arquivo.bin") {
Ok(cab) => println!("Cabeçalho: {:02X?}", cab),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
eprintln!("Arquivo menor que 8 bytes");
}
Err(e) => return Err(e),
}
Ok(())
}
Escrita básica com Write
use std::fs::File;
use std::io::{self, Write};
fn main() -> io::Result<()> {
let mut arquivo = File::create("saida.txt")?;
// write() — pode escrever parcialmente, retorna quantos bytes escreveu
let n = arquivo.write(b"Linha parcial possivel")?;
println!("Escritos {} bytes", n);
// write_all() — garante que TODOS os bytes sejam escritos
arquivo.write_all(b"\nLinha completa garantida\n")?;
// write! e writeln! — escrita formatada via write_fmt()
let nome = "Rust Brasil";
let versao = 2026;
writeln!(arquivo, "Projeto: {} ({})", nome, versao)?;
// flush() — força envio de dados bufferizados
arquivo.flush()?;
println!("Arquivo escrito com sucesso");
Ok(())
}
Tabela de métodos
Métodos de Read
| Método | Retorno | Descrição |
|---|---|---|
read(&mut buf) | io::Result<usize> | Lê bytes no buffer; pode ler menos que o buffer |
read_to_end(&mut vec) | io::Result<usize> | Lê todos os bytes até EOF em um Vec<u8> |
read_to_string(&mut s) | io::Result<usize> | Lê tudo como UTF-8 em uma String |
read_exact(&mut buf) | io::Result<()> | Lê exatamente buf.len() bytes ou falha |
take(limit) | Take<Self> | Limita leitura a N bytes |
chain(next) | Chain<Self, R> | Encadeia dois leitores sequencialmente |
bytes() | Bytes<Self> | Iterador byte a byte |
by_ref() | &mut Self | Referência mutável para uso temporário |
Métodos de Write
| Método | Retorno | Descrição |
|---|---|---|
write(&buf) | io::Result<usize> | Escreve bytes; pode escrever parcialmente |
write_all(&buf) | io::Result<()> | Escreve todos os bytes ou falha |
write_fmt(args) | io::Result<()> | Escrita formatada (usado por write!/writeln!) |
flush() | io::Result<()> | Envia dados bufferizados ao destino |
by_ref() | &mut Self | Referência mutável para uso temporário |
Exemplos práticos
Exemplo 1: Função genérica que aceita qualquer Read
use std::io::{self, BufRead, BufReader, Read};
/// Conta palavras em qualquer fonte que implemente Read
fn contar_palavras<R: Read>(fonte: R) -> io::Result<usize> {
let leitor = BufReader::new(fonte);
let mut total = 0;
for linha in leitor.lines() {
let linha = linha?;
total += linha.split_whitespace().count();
}
Ok(total)
}
fn main() -> io::Result<()> {
// Com arquivo
let arquivo = std::fs::File::open("documento.txt")?;
println!("Palavras no arquivo: {}", contar_palavras(arquivo)?);
// Com buffer em memória (para testes)
let texto = b"Rust e seguro e rapido.\nAprenda Rust hoje.";
let cursor = io::Cursor::new(texto);
println!("Palavras no texto: {}", contar_palavras(cursor)?);
Ok(())
}
Exemplo 2: Implementando Read para um tipo customizado
use std::io::{self, Read};
/// Leitor que gera uma sequência numérica como texto
struct GeradorSequencia {
atual: u64,
limite: u64,
buffer_interno: Vec<u8>,
posicao: usize,
}
impl GeradorSequencia {
fn new(limite: u64) -> Self {
Self {
atual: 0,
limite,
buffer_interno: Vec::new(),
posicao: 0,
}
}
}
impl Read for GeradorSequencia {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// Reabastecer buffer interno se necessário
if self.posicao >= self.buffer_interno.len() {
if self.atual >= self.limite {
return Ok(0); // EOF
}
self.buffer_interno = format!("{}\n", self.atual).into_bytes();
self.posicao = 0;
self.atual += 1;
}
let disponivel = &self.buffer_interno[self.posicao..];
let n = buf.len().min(disponivel.len());
buf[..n].copy_from_slice(&disponivel[..n]);
self.posicao += n;
Ok(n)
}
}
fn main() -> io::Result<()> {
let mut gerador = GeradorSequencia::new(5);
let mut saida = String::new();
gerador.read_to_string(&mut saida)?;
println!("Sequência gerada:\n{}", saida);
// Saída: 0\n1\n2\n3\n4\n
Ok(())
}
Exemplo 3: Implementando Write para um tipo customizado
use std::io::{self, Write};
/// Escritor que conta estatísticas enquanto escreve
struct EscritorEstatistico<W: Write> {
interno: W,
bytes_totais: usize,
chamadas_write: usize,
chamadas_flush: usize,
}
impl<W: Write> EscritorEstatistico<W> {
fn new(interno: W) -> Self {
Self {
interno,
bytes_totais: 0,
chamadas_write: 0,
chamadas_flush: 0,
}
}
fn relatorio(&self) -> String {
format!(
"Bytes: {}, writes: {}, flushes: {}",
self.bytes_totais, self.chamadas_write, self.chamadas_flush
)
}
}
impl<W: Write> Write for EscritorEstatistico<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.interno.write(buf)?;
self.bytes_totais += n;
self.chamadas_write += 1;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.chamadas_flush += 1;
self.interno.flush()
}
}
fn main() -> io::Result<()> {
let destino = Vec::new();
let mut escritor = EscritorEstatistico::new(destino);
writeln!(escritor, "Primeira linha")?;
writeln!(escritor, "Segunda linha com mais dados")?;
escritor.write_all(b"Terceira linha\n")?;
escritor.flush()?;
println!("{}", escritor.relatorio());
// Saída: Bytes: 60, writes: 3, flushes: 1
Ok(())
}
Exemplo 4: Encadeando leitores com chain e limitando com take
use std::io::{self, Read};
fn main() -> io::Result<()> {
let prefixo = io::Cursor::new(b"HEADER|" as &[u8]);
let dados = io::Cursor::new(b"conteudo_real_dos_dados_do_sistema" as &[u8]);
let sufixo = io::Cursor::new(b"|FOOTER" as &[u8]);
// Encadear três fontes
let combinado = prefixo.chain(dados).chain(sufixo);
// Limitar a leitura total a 30 bytes
let mut limitado = combinado.take(30);
let mut resultado = String::new();
limitado.read_to_string(&mut resultado)?;
println!("Resultado (max 30 bytes): '{}'", resultado);
println!("Tamanho: {} bytes", resultado.len());
Ok(())
}
Exemplo 5: Transformar dados entre Read e Write
use std::io::{self, BufRead, BufReader, Write};
/// Converte texto para maiúsculas lendo de qualquer Read e escrevendo em qualquer Write
fn converter_maiusculas<R: io::Read, W: Write>(entrada: R, mut saida: W) -> io::Result<u64> {
let leitor = BufReader::new(entrada);
let mut bytes_escritos: u64 = 0;
for linha in leitor.lines() {
let linha_upper = linha?.to_uppercase();
let dados = format!("{}\n", linha_upper);
saida.write_all(dados.as_bytes())?;
bytes_escritos += dados.len() as u64;
}
saida.flush()?;
Ok(bytes_escritos)
}
fn main() -> io::Result<()> {
let entrada = io::Cursor::new("olá mundo\nrust é incrível\n");
let mut saida = Vec::new();
let n = converter_maiusculas(entrada, &mut saida)?;
println!("Convertidos {} bytes:", n);
println!("{}", String::from_utf8_lossy(&saida));
// Também funciona com arquivos:
// let entrada = File::open("entrada.txt")?;
// let saida = File::create("saida.txt")?;
// converter_maiusculas(entrada, saida)?;
Ok(())
}
Padrões de tratamento de erro para I/O
write() vs write_all()
Uma armadilha comum é usar write() esperando que todos os bytes sejam escritos. O write() pode escrever parcialmente — use write_all() para garantia:
use std::io::{self, Write};
fn escrita_segura<W: Write>(mut destino: W, dados: &[u8]) -> io::Result<()> {
// ERRADO: write() pode não escrever tudo
// let _ = destino.write(dados)?;
// CORRETO: write_all() garante escrita completa
destino.write_all(dados)?;
destino.flush()?;
Ok(())
}
Tratando read() retornando 0
Um retorno de 0 em read() significa fim do fluxo (EOF). Sempre trate esse caso:
use std::io::{self, Read};
fn ler_em_blocos<R: Read>(mut fonte: R) -> io::Result<Vec<u8>> {
let mut resultado = Vec::new();
let mut buffer = [0u8; 4096];
loop {
match fonte.read(&mut buffer) {
Ok(0) => break, // EOF — parar
Ok(n) => resultado.extend_from_slice(&buffer[..n]),
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(resultado)
}
Dicas de desempenho
- Envolva
ReadcomBufReadereWritecomBufWriterpara reduzir syscalls quando fizer muitas operações pequenas. - Use
read_to_endcomVec::with_capacityse souber o tamanho aproximado dos dados para evitar realocações. - Prefira
write_alla múltiploswrite— evita a lógica de loop manual para escrita completa. - Use
io::copypara transferências diretas — é otimizado internamente e evita alocações intermediárias.
Veja também
- Módulo std::io — visão geral do módulo de I/O
- BufReader e BufWriter — I/O bufferizado para performance
- File e OpenOptions — trabalhar com arquivos no disco
- Ler Arquivo em Rust — receita prática de leitura
- Escrever em Arquivo — receita prática de escrita
- Documentação oficial:
std::io::Read,std::io::Write