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 match para ações diferentes em caso de sucesso e erro.
  • Usar if let quando apenas um caso interessa.
  • Combinar ? para propagar erros em funções que retornam Result.
  • Usar map, and_then, or_else para 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

Exercícios

  1. Escreva uma função que recebe uma string e tenta convertê-la para um inteiro. Retorne Result<i32, String>.
  2. ✓ Resposta:
    fn parse_int(s: &str) -> Result<i32, String> {
        s.trim().parse::<i32>()
            .map_err(|_| format!("Não foi possível converter '{}' para inteiro", s))
    }
  3. Use o operador ? para escrever uma função que lê um arquivo e retorna seu conteúdo como string, propagando erros.
  4. ✓ Resposta:
    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)
    }
  5. 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.
  6. ✓ Resposta:
    #[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())),
        }
    }
  7. Explique a diferença entre unwrap e expect e dê um exemplo onde expect é preferível.
  8. ✓ Resposta: Ambos causam pânico se o Result for Err, mas 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.
  9. Use map e and_then para encadear duas operações que retornam Result: primeiro parseie uma string para inteiro, depois calcule o quadrado.
  10. ✓ Resposta:
    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))
    }