O tipo Result
Esta aula aborda o tipo Result do Rust, que é usado para operações que podem falhar. Você aprenderá a usar Ok e Err, propagar erros com o operador ?, comparar expect e unwrap, e aplicar tratamento idiomático de erros.
O tipo Result é uma enumeração fundamental em Rust para representar o sucesso ou falha de uma operação. Diferente de exceções em outras linguagens, Result é um tipo de dado que deve ser tratado explicitamente, garantindo que erros não passem despercebidos.
Nesta aula, exploraremos as variantes Ok e Err, a propagação de erros com o operador ?, os métodos expect e unwrap, e as práticas idiomáticas para lidar com resultados.
Ok e Err
Result é definido como enum Result<T, E> { Ok(T), Err(E) }. Ok(T) indica sucesso e contém um valor do tipo T, enquanto Err(E) indica falha e contém um erro do tipo E. É usado em funções que podem falhar, como operações de I/O, parsing, etc.
Exemplo: uma função que divide dois números, retornando um erro se o divisor for zero.
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("divisão por zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Resultado: {}", result),
Err(e) => println!("Erro: {}", e),
}
}O padrão match é uma forma segura de lidar com Result, forçando o tratamento de ambos os casos.
Propagação com ?
O operador ? é uma forma concisa de propagar erros. Se o valor for Ok, ele desempacota o valor interno; se for Err, retorna o erro da função atual (desde que o tipo de retorno seja compatível).
Exemplo: leitura de um arquivo e parsing de um número.
use std::fs::File;
use std::io::{self, Read};
fn read_number_from_file(path: &str) -> Result<i32, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let number: i32 = contents.trim().parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(number)
}Note que parse retorna Result<i32, ParseIntError>, mas convertemos o erro para io::Error com map_err para manter a consistência. O operador ? só funciona em funções que retornam Result (ou Option).
expect vs unwrap
unwrap e expect são métodos que extraem o valor de Ok ou causam pânico se for Err. unwrap não fornece mensagem personalizada; expect permite uma mensagem descritiva.
fn main() {
let x: Result<i32, &str> = Ok(10);
println!("{}", x.unwrap()); // 10
let y: Result<i32, &str> = Err("erro");
// y.unwrap(); // pânico
y.expect("falha ao obter valor"); // pânico com mensagem
}Esses métodos são úteis em protótipos ou quando se tem certeza de que o resultado é Ok, mas em código de produção é preferível tratamento explícito com match ou ?.
Tratamento idiomático
Rust incentiva o tratamento explícito de erros. Padrões idiomáticos incluem:
- Usar
matchpara ações diferentes em caso de sucesso e erro. - Usar
if letquando apenas um caso interessa. - Combinar
?para propagar erros em funções que retornamResult. - Usar
map,and_then,or_elsepara encadeamento.
Exemplo com if let:
fn main() {
let result = divide(10.0, 0.0);
if let Ok(val) = result {
println!("Sucesso: {}", val);
} else {
println!("Falhou");
}
}Encadeamento com and_then:
fn main() {
let result = divide(10.0, 2.0)
.and_then(|v| divide(v, 2.0));
match result {
Ok(v) => println!("Resultado final: {}", v),
Err(e) => println!("Erro: {}", e),
}
}Boas práticas: sempre documente os erros que sua função pode retornar; use tipos de erro personalizados (como enum) para categorizar falhas; evite unwrap/expect em código de produção; prefira ? para propagação.
Referências
- Documentação oficial do Result
- The Rust Book: Recoverable Errors with Result
- Rust by Example: Result
- Documentação de unwrap
- Documentação de expect
Exercícios
- Escreva uma função que recebe uma string e tenta convertê-la para um inteiro. Retorne
Result<i32, String>. - Use o operador
?para escrever uma função que lê um arquivo e retorna seu conteúdo como string, propagando erros. - Crie um enum de erro personalizado para uma calculadora simples (operações: soma, subtração, multiplicação, divisão). Use-o em uma função que realiza a operação.
- Explique a diferença entre
unwrapeexpecte dê um exemplo ondeexpecté preferível. - Use
mapeand_thenpara encadear duas operações que retornam Result: primeiro parseie uma string para inteiro, depois calcule o quadrado.
fn parse_int(s: &str) -> Result<i32, String> {
s.trim().parse::<i32>()
.map_err(|_| format!("Não foi possível converter '{}' para inteiro", s))
}use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> io::Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}#[derive(Debug)]
enum CalcError {
DivisionByZero,
InvalidOperation(String),
}
fn calculate(op: &str, a: f64, b: f64) -> Result<f64, CalcError> {
match op {
"+" => Ok(a + b),
"-" => Ok(a - b),
"*" => Ok(a * b),
"/" => if b == 0.0 { Err(CalcError::DivisionByZero) } else { Ok(a / b) },
_ => Err(CalcError::InvalidOperation(op.to_string())),
}
}expect permite uma mensagem de erro personalizada, facilitando o diagnóstico. Exemplo: let valor = arquivo.leia().expect("Falha ao ler arquivo de configuração"); é melhor que unwrap() pois a mensagem explica o contexto.fn square_of_string(s: &str) -> Result<i32, String> {
s.trim().parse::<i32>()
.map_err(|_| format!("Parse error: {}", s))
.and_then(|n| Ok(n * n))
}