👋 Bem-vindos, desbravadores! Hoje vamos explorar um dos aspectos mais poderosos e característicos de Rust: seu sistema de tratamento de erros. Se você vem do Python, onde usamos exceções para quase tudo, prepare-se para uma abordagem mais segura e explícita que vai mudar sua forma de pensar sobre erros.
No Python, estamos acostumados com o clássico try/except - lançamos exceções quando algo dá errado e as capturamos quando queremos lidar com os problemas. É prático, mas também propenso a erros: podemos facilmente esquecer de tratar um erro ou deixar escapar exceções inesperadas.
Rust toma um caminho diferente: erros são tratados como valores, não como exceções controladas. Isso significa que o compilador nos força a lidar com possíveis falhas explicitamente, tornando nosso código mais seguro e previsível.
Vamos comparar as duas abordagens:
Em Python, usamos None para representar a ausência de valor. Em Rust, temos Option<T>, que é muito mais poderoso e seguro.
Option<T> é um enum (tipo enumerado) que pode ser:
Some(T) - contém um valor do tipo TNone - não contém nenhum valorVejamos a diferença na prática:
# Python: Função que pode retornar None
def encontrar_primeiro_par(numeros):
for num in numeros:
if num % 2 == 0:
return num
return None
# Uso (podemos esquecer de verificar None)
resultado = encontrar_primeiro_par([1, 3, 5])
if resultado is not None:
print(f"Encontrado: {resultado}")
else:
print("Nenhum par encontrado")
// Rust: Função que retorna Option<i32>
fn encontrar_primeiro_par(numeros: &[i32]) -> Option<i32> {
for &num in numeros {
if num % 2 == 0 {
return Some(num); // Temos um valor
}
}
None // Nenhum valor encontrado
}
// Uso (o compilador nos força a tratar ambos os casos)
fn main() {
let numeros = vec![1, 3, 5];
match encontrar_primeiro_par(&numeros) {
Some(num) => println!("Encontrado: {}", num),
None => println!("Nenhum par encontrado"),
}
}
A grande vantagem do Option é que o compilador não deixa você esquecer de tratar o caso None. Em Python, é fácil esquecer de verificar se um valor é None - em Rust, isso é impossível.
Rust oferece vários métodos para trabalhar com Option de forma concisa:
let valor_some = Some(42);
let valor_none: Option<i32> = None;
// unwrap(): obtém o valor ou entra em pânico se for None (⚠️ perigoso!)
println!("{}", valor_some.unwrap()); // 42
// println!("{}", valor_none.unwrap()); // PANIC!
// unwrap_or(): valor padrão se for None
println!("{}", valor_none.unwrap_or(0)); // 0
// map(): transforma o valor se for Some
let valor_dobrado = valor_some.map(|x| x * 2); // Some(84)
// and_then(): transforma e "achata" o resultado
let resultado = valor_some.and_then(|x| Some(x * 2)); // Some(84)
Enquanto Option lida com a ausência de valor, Result<T, E> lida com operações que podem falhar. É o equivalente rusticano das exceções do Python, mas muito mais seguro.
Result é um enum com duas variantes:
Ok(T) - operação bem-sucedida, contém o resultadoErr(E) - operação falhou, contém informação do erroVamos comparar com Python:
# Python: Divisão com possível exceção
def dividir(a, b):
if b == 0:
raise ValueError("Não pode dividir por zero!")
return a / b
# Uso (podemos esquecer de capturar a exceção)
try:
resultado = dividir(10, 0)
print(f"Resultado: {resultado}")
except ValueError as e:
print(f"Erro: {e}")
// Rust: Divisão com Result
fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
return Err("Não pode dividir por zero!".to_string());
}
Ok(a / b)
}
// Uso (devemos tratar ambos os casos)
fn main() {
match dividir(10.0, 0.0) {
Ok(resultado) => println!("Resultado: {}", resultado),
Err(erro) => println!("Erro: {}", erro),
}
}
Em Rust, podemos (e devemos!) criar nossos próprios tipos de erro:
// Definindo um tipo de erro personalizado
#[derive(Debug)]
enum ErroMatematico {
DivisaoPorZero,
RaizNegativa,
Overflow,
}
// Implementando mensagem de erro
impl std::fmt::Display for ErroMatematico {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ErroMatematico::DivisaoPorZero => write!(f, "Divisão por zero"),
ErroMatematico::RaizNegativa => write!(f, "Raiz quadrada de número negativo"),
ErroMatematico::Overflow => write!(f, "Overflow matemático"),
}
}
}
// Função que usa nosso erro personalizado
fn dividir_seguro(a: f64, b: f64) -> Result<f64, ErroMatematico> {
if b == 0.0 {
return Err(ErroMatematico::DivisaoPorZero);
}
Ok(a / b)
}
O match é uma das ferramentas mais poderosas de Rust para tratar Option e Result:
fn processar_resultado(resultado: Result<i32, String>) {
match resultado {
Ok(valor) => {
println!("Sucesso! Valor: {}", valor);
// Podemos fazer mais processamento aqui
},
Err(erro) => {
println!("Falha! Erro: {}", erro);
// Podemos tratar o erro ou propagar
}
}
}
// Match também funciona com Option
fn processar_option(opcao: Option<String>) {
match opcao {
Some(texto) => println!("Texto: {}", texto),
None => println!("Nenhum texto fornecido"),
}
}
O operador ? é uma das características mais convenientes de Rust. Ele propaga erros automaticamente:
// Sem o operador ? (mais verboso)
fn ler_arquivo_caminho(caminho: &str) -> Result<String, std::io::Error> {
let arquivo_resultado = std::fs::File::open(caminho);
let mut arquivo = match arquivo_resultado {
Ok(arquivo) => arquivo,
Err(erro) => return Err(erro),
};
let mut conteudo = String::new();
match std::io::Read::read_to_string(&mut arquivo, &mut conteudo) {
Ok(_) => Ok(conteudo),
Err(erro) => Err(erro),
}
}
// Com o operador ? (muito mais limpo!)
fn ler_arquivo_caminho_simples(caminho: &str) -> Result<String, std::io::Error> {
let mut arquivo = std::fs::File::open(caminho)?;
let mut conteudo = String::new();
std::io::Read::read_to_string(&mut arquivo, &mut conteudo)?;
Ok(conteudo)
}
// Podemos ainda simplificar mais com métodos de conveniência
fn ler_arquivo_mais_simples(caminho: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(caminho)
}
| Característica | Python | Rust |
|---|---|---|
| Propagação | Automática (call stack) | Manual (com ?) |
| Verificação | Em runtime | Em tempo de compilação |
| Obrigatoriedade | Opcional tratar | Obrigatório tratar |
| Performance | Custo alto (stack unwinding) | Custo zero (erros são valores) |
Em Python, use exceções para:
Em Rust, use Result/Option para:
Vamos ver um exemplo completo que processa um arquivo de configuração:
# Python: Processador de configuração com exceções
import json
def carregar_configuracao(caminho):
try:
with open(caminho, 'r') as arquivo:
config = json.load(arquivo)
if 'porta' not in config:
raise ValueError("Porta não especificada na configuração")
if not isinstance(config['porta'], int):
raise TypeError("Porta deve ser um número inteiro")
return config
except FileNotFoundError:
print(f"Arquivo {caminho} não encontrado")
return {"porta": 8080} # Valor padrão
except json.JSONDecodeError:
print("Erro ao decodificar JSON")
return {"porta": 8080}
except (ValueError, TypeError) as e:
print(f"Erro de configuração: {e}")
return {"porta": 8080}
# Uso
config = carregar_configuracao("config.json")
print(f"Porta: {config['porta']}")
// Rust: Processador de configuração com Result
use std::fs;
use serde::Deserialize; // ⚠️ Necessário adicionar serde no Cargo.toml
#[derive(Debug, Deserialize)]
struct Configuracao {
porta: u16,
}
// Definimos nossos tipos de erro
#[derive(Debug)]
enum ErroConfiguracao {
ArquivoNaoEncontrado,
ParseErro,
PortaNaoEspecificada,
PortaInvalida,
}
// Implementamos tratamento de erro personalizado
impl std::fmt::Display for ErroConfiguracao {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ErroConfiguracao::ArquivoNaoEncontrado => write!(f, "Arquivo não encontrado"),
ErroConfiguracao::ParseErro => write!(f, "Erro ao analisar JSON"),
ErroConfiguracao::PortaNaoEspecificada => write!(f, "Porta não especificada"),
ErroConfiguracao::PortaInvalida => write!(f, "Porta deve ser um número válido"),
}
}
}
fn carregar_configuracao(caminho: &str) -> Result<Configuracao, ErroConfiguracao> {
// Usamos ? para propagar erros automaticamente
let conteudo = fs::read_to_string(caminho)
.map_err(|_| ErroConfiguracao::ArquivoNaoEncontrado)?;
// Parse do JSON com tratamento de erro
let config: Configuracao = serde_json::from_str(&conteudo)
.map_err(|_| ErroConfiguracao::ParseErro)?;
// Validação adicional
if config.porta == 0 {
return Err(ErroConfiguracao::PortaInvalida);
}
Ok(config)
}
// Função principal com tratamento de erro
fn main() {
match carregar_configuracao("config.json") {
Ok(config) => println!("Porta: {}", config.porta),
Err(ErroConfiguracao::ArquivoNaoEncontrado) => {
println!("Usando configuração padrão (porta 8080)");
let config_default = Configuracao { porta: 8080 };
println!("Porta: {}", config_default.porta);
},
Err(erro) => {
println!("Erro na configuração: {}. Usando padrão (porta 8080)", erro);
let config_default = Configuracao { porta: 8080 };
println!("Porta: {}", config_default.porta);
}
}
}
match oferece controle preciso sobre o fluxo de errosO sistema de erros de Rust pode parecer verboso no início, especialmente vindo do Python, mas essa “verborragia” é na verdade explícita documentação em tempo de compilação. O compilator é seu amigo aqui, garantindo que você nunca se esqueça de tratar um erro possível.
Quer se aprofundar ainda mais em Rust? O livro “Desbravando Rust” cobre esses e muitos outros conceitos com exemplos práticos, exercícios e projetos reais.
Visite nosso site desbravandorust.com.br para adquirir seu exemplar e continuar sua jornada na linguagem mais amada pela comunidade!
Nos próximos posts, vamos explorar concorrência em Rust, sistemas de tipos avançados, e como interoperar Rust com Python. Até lá! 🚀
Artigo publicado no blog Desbravando Rust - Material de apoio para o livro “Desbravando Rust”