O Operador ? em Rust: Propagação de Erros Elegante
O que é o Operador ?
O operador ? é um atalho poderoso em Rust para propagação de erros. Quando usado em uma função que retorna Result<T, E> ou Option<T>, ele funciona como um "desempacotador inteligente": se o valor é um sucesso (Ok ou Some), ele extrai o conteúdo; se for um erro (Err ou None), ele imediatamente retorna esse erro da função atual. Isso elimina a necessidade de escrever condicionais verbosos com match ou unwrap, tornando o código mais legível e seguro.
O ? foi introduzido no Rust 1.13 como resposta à necessidade de tratamento de erros mais ergonômico. Em vez de usar try! (macro anterior), você escreve código que flui naturalmente enquanto mantém a segurança de tipos que Rust oferece.
use std::fs;
use std::io;
// Sem o operador ?
fn ler_arquivo_verboso(caminho: &str) -> Result<String, io::Error> {
let conteudo = match fs::read_to_string(caminho) {
Ok(dados) => dados,
Err(e) => return Err(e),
};
Ok(conteudo)
}
// Com o operador ?
fn ler_arquivo(caminho: &str) -> Result<String, io::Error> {
let conteudo = fs::read_to_string(caminho)?;
Ok(conteudo)
}
Sintaxe e Casos de Uso
O operador ? pode ser usado em qualquer função que retorna Result<T, E> ou Option<T>. A sintaxe é simples: coloque ? imediatamente após a expressão que você quer desempacotar. Você não pode usar ? em funções que retornam () (void) — a função precisa retornar um tipo que implemente o trait Try.
Um detalhe importante: quando você usa ? com Result, o erro é automaticamente convertido usando o trait From. Isso significa que se sua função retorna Result<T, MinhaError>, mas uma operação retorna Result<T, OutraError>, Rust tentará converter OutraError em MinhaError usando From::from(). Isso é extraordinariamente conveniente para consolidar múltiplos tipos de erro.
use std::fs;
use std::num::ParseIntError;
#[derive(Debug)]
enum MinhaError {
IoError(std::io::Error),
ParseError(ParseIntError),
}
impl From<std::io::Error> for MinhaError {
fn from(err: std::io::Error) -> Self {
MinhaError::IoError(err)
}
}
impl From<ParseIntError> for MinhaError {
fn from(err: ParseIntError) -> Self {
MinhaError::ParseError(err)
}
}
fn processar_arquivo(caminho: &str) -> Result<i32, MinhaError> {
let conteudo = fs::read_to_string(caminho)?; // Pode retornar IoError
let numero = conteudo.trim().parse::<i32>()?; // Pode retornar ParseIntError
Ok(numero)
}
Operador ? com Option
Além de Result, o operador ? também funciona com Option<T>. Quando você usa ? em um Option, se for None, a função retorna None imediatamente. Isso é útil para cadeias de operações opcionais onde qualquer None significa que não há resultado válido.
A combinação de ? com métodos que retornam Option torna código que busca valores aninhados extremamente limpo. Por exemplo, navegar por estruturas de dados aninhadas que podem não existir fica muito mais expressivo do que usar múltiplos if let ou match.
fn extrair_idade(dados: Option<(&str, u32)>) -> Option<u32> {
let (_nome, idade) = dados?;
let idade_dobrada = idade.checked_mul(2)?; // Retorna None se overflow
Some(idade_dobrada)
}
fn main() {
println!("{:?}", extrair_idade(Some(("Alice", 25)))); // Some(50)
println!("{:?}", extrair_idade(None)); // None
}
Boas Práticas e Limitações
O operador ? não é uma bala de prata. Há situações onde match ou if let são mais apropriados: quando você precisa fazer algo específico com o erro (logging, recuperação parcial) antes de propagar, ou quando precisa decidir entre múltiplos caminhos baseado no tipo exato do erro. Use ? para o caminho "feliz" direto; use match para lógica condicional complexa.
Uma limitação prática: você não pode usar ? em closures que retornam tipos simples. Se um closure retorna (), você não pode usar ? dentro dele. Para isso, use .map_err() ou converta a closure em uma função separada. Além disso, em main(), você não pode usar ? diretamente — a função main retorna (). Porém, em Rust moderno, você pode fazer fn main() -> Result<(), Box<dyn std::error::Error>> para permitir ?.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dados = std::fs::read_to_string("config.txt")?;
let numero: i32 = dados.trim().parse()?;
println!("Valor: {}", numero);
Ok(())
}
Conclusão
O operador ? em Rust é um exemplo brilhante de design de linguagem: fornece conveniência sem sacrificar segurança. Ele elimina boilerplate verboso enquanto força o programador a estar ciente de que erros podem ocorrer. Aprenda a usá-lo naturalmente para seu código fluir entre operações que podem falhar, combine-o com traits From customizados para consolidar tipos de erro, e reserve match para lógica que exige decisões explícitas sobre cada tipo de falha.