O tipo Result
Esta aula aborda o tipo Result do Rust, que é a principal forma de lidar com operações que podem falhar. Serão explicados os variantes Ok e Err, a propagação de erros com o operador ?, e as diferenças entre expect e unwrap, além de padrões idiomáticos para tratamento de erros.
O tipo Result é um dos tipos mais importantes do Rust, usado para representar o resultado de uma operação que pode ter sucesso ou falhar. Ele é definido como um enum com duas variantes: Ok(T) para sucesso contendo um valor do tipo T, e Err(E) para erro contendo um valor do tipo E. Diferente de exceções em outras linguagens, o Rust força o programador a lidar com a possibilidade de erro explicitamente, tornando o código mais seguro e previsível.
Nesta aula, veremos como criar, manipular e propagar resultados, além de boas práticas para tratamento de erros de forma idiomática.
Ok e Err
As variantes Ok e Err são as únicas formas de construir um valor do tipo Result. Ok indica que a operação foi bem-sucedida e carrega o valor resultante. Err indica que houve uma falha e carrega um valor que descreve o erro.
Por exemplo, ao abrir um arquivo, a função File::open retorna um Result. Se o arquivo existe, retorna Ok(file); caso contrário, retorna Err(error).
use std::fs::File;
fn main() {
let result = File::open("hello.txt");
match result {
Ok(file) => println!("Arquivo aberto: {:?}", file),
Err(error) => println!("Erro ao abrir arquivo: {}", error),
}
}
É comum usar match para tratar ambos os casos, mas existem métodos auxiliares como unwrap e expect que simplificam o código em situações onde o erro é inesperado.
Propagação com ?
O operador ? é uma forma concisa de propagar erros. Quando usado em uma expressão que retorna Result, ele desempacota o valor se for Ok ou retorna o erro imediatamente da função atual. Isso evita a verbosidade de match em cadeia.
Para usar ?, a função deve retornar um tipo que implemente FromResidual (geralmente Result ou Option). Exemplo:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?; // Se Err, retorna o erro
let mut username = String::new();
file.read_to_string(&mut username)?; // Se Err, retorna o erro
Ok(username)
}
O operador ? também pode ser usado em cadeia: File::open("file.txt")?.read_to_string(&mut s)?;. Ele é especialmente útil em funções que lidam com múltiplas operações que podem falhar.
expect vs unwrap
Os métodos unwrap e expect são atalhos para obter o valor de um Ok ou causar um pânico se for Err. A diferença é que expect permite fornecer uma mensagem de erro personalizada, enquanto unwrap usa uma mensagem padrão.
Ambos devem ser usados com cautela, pois causam pânico (encerramento do programa) em caso de erro. São apropriados em protótipos, testes, ou quando se tem certeza de que o erro não ocorrerá (ex.: ao ler uma variável de ambiente que sempre existe).
fn main() {
// unwrap: mensagem genérica
let num: i32 = "42".parse().unwrap();
println!("Número: {}", num);
// expect: mensagem personalizada
let file = File::open("important.txt")
.expect("Falha ao abrir o arquivo important.txt");
}
Em código de produção, prefira tratamento explícito com match ou ? para evitar pânicos inesperados.
Tratamento idiomático
O Rust possui um ecossistema rico para tratamento de erros. Além de Result, existem bibliotecas como anyhow e thiserror que facilitam a criação e propagação de erros em aplicações maiores.
Para código idiomático, siga estas práticas:
- Use
?para propagar erros na maioria dos casos. - Defina seus próprios tipos de erro usando enum com
#[derive(Debug)]e implementestd::fmt::Displayestd::error::Error. - Use
matchpara tratamento específico de cada variante de erro. - Considere usar
map_errpara converter tipos de erro. - Em funções main, use
Result<(), Boxpara permitir o uso de> ?.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let _file = File::open("config.txt")?;
Ok(())
}
Outra abordagem comum é usar match com guardas para tratar erros específicos:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let result = File::open("hello.txt");
let file = match result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problema ao criar arquivo: {:?}", e),
},
other_error => panic!("Erro ao abrir arquivo: {:?}", other_error),
},
};
}
Boas práticas
Sempre prefira o operador ? para propagar erros, a menos que precise de tratamento específico. Evite unwrap e expect em código de produção, a menos que você tenha certeza absoluta de que o erro não ocorrerá. Documente os erros que sua função pode retornar, especialmente se for parte de uma API pública.
Referências
- The Rust Book: Recoverable Errors with Result
- Documentação oficial do tipo Result
- Rust by Example: Result
- The Rust Book: To panic! or Not to panic!
- Documentação do trait Error
- anyhow crate
- thiserror crate
Exercícios
-
Escreva uma função que lê um número inteiro de um arquivo e retorna
Result. Use o operador?para propagar erros.✓ Resposta: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 num: i32 = contents.trim().parse() .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; Ok(num) } -
Dado um
Result, usematchpara imprimir o valor se forOkou a mensagem de erro se forErr.✓ Resposta:fn main() { let result: Result<i32, String> = Ok(42); match result { Ok(val) => println!("Valor: {}", val), Err(err) => println!("Erro: {}", err), } } -
Converta o código abaixo para usar o operador
?em vez dematchexplícito:use std::fs::File; fn open_file() -> Result<File, std::io::Error> { let file = match File::open("foo.txt") { Ok(f) => f, Err(e) => return Err(e), }; Ok(file) }✓ Resposta:use std::fs::File; fn open_file() -> Result<File, std::io::Error> { let file = File::open("foo.txt")?; Ok(file) } -
Escreva um programa que tenta converter uma string em número usando
parsee usaexpectcom uma mensagem adequada. Depois, comente por que isso não é seguro.✓ Resposta:fn main() { let input = "42a"; let num: i32 = input.parse() .expect("Falha ao converter string para número"); println!("Número: {}", num); } // Isso não é seguro porque se a string não for um número válido, o programa entra em pânico e para abruptamente. -
Crie um enum de erro personalizado chamado
MyErrorcom variantesIo(std::io::Error)eParse(std::num::ParseIntError). ImplementeDisplayeError. Depois, usemap_errpara converter erros deFile::openeparseparaMyErrorem uma função que retornaResult.✓ Resposta:use std::error::Error; use std::fmt; use std::fs::File; use std::io::Read; #[derive(Debug)] enum MyError { Io(std::io::Error), Parse(std::num::ParseIntError), } impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyError::Io(e) => write!(f, "IO error: {}", e), MyError::Parse(e) => write!(f, "Parse error: {}", e), } } } impl Error for MyError {} fn read_number() -> Result<i32, MyError> { let mut file = File::open("number.txt").map_err(MyError::Io)?; let mut contents = String::new(); file.read_to_string(&mut contents).map_err(MyError::Io)?; let num: i32 = contents.trim().parse().map_err(MyError::Parse)?; Ok(num) } fn main() { match read_number() { Ok(n) => println!("Número: {}", n), Err(e) => println!("Erro: {}", e), } }