Pular para o conteúdo

Result em Rust

Se o Option representa um valor que pode estar ausente, o Result representa uma operação que pode falhar. E, ao contrário de um valor ausente, uma falha costuma vir acompanhada de um motivo.

Result é mais um enum da biblioteca padrão, desta vez com duas variantes que carregam dados:

enum Result<T, E> {
Ok(T), // sucesso, com um valor
Err(E), // falha, com um erro
}

O .parse(), que você conheceu na página de Conversão de tipos, é um bom exemplo real: transformar texto em número pode falhar se o texto não for, de fato, um número:

fn main() {
let good: Result<i32, std::num::ParseIntError> = "42".parse();
let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good);
println!("{:?}", bad);
}

Saída:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Experimente você mesmo

O InvalidDigit (dígito inválido) vem do próprio Rust: ele está dizendo que encontrou um caractere que não é um dígito.

fn main() {
let input = "42";
match input.parse::<i32>() {
Ok(number) => println!("Convertido: {number}"),
Err(e) => println!("Falha na conversão: {e}"),
}
}
▶ Experimente você mesmo

É o mesmo formato de tratar um Option com match. A diferença é que o braço Err entrega um valor de erro de verdade, que você pode examinar ou mostrar, e não só um “não tinha nada aqui”.

Escrevendo sua própria função que devolve Result

Seção intitulada “Escrevendo sua própria função que devolve Result”
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("não é possível dividir por zero"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Resultado: {result}"),
Err(e) => println!("Erro: {e}"),
}
match divide(10.0, 0.0) {
Ok(result) => println!("Resultado: {result}"),
Err(e) => println!("Erro: {e}"),
}
}

Saída:

Resultado: 5
Erro: não é possível dividir por zero
▶ Experimente você mesmo

Devolver Result<f64, String> obriga quem chama a função a lidar com a possibilidade de falha. Não tem como tratar uma divisão por zero como se fosse um número normal sem perceber.

Encadear várias operações que podem falhar leva a blocos match aninhados e difíceis de ler. O operador ? resolve isso. Ele quer dizer: “se isto for Err, devolva esse erro imediatamente da função atual; se não for, me dê o valor que está dentro do Ok”.

fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let number = input.parse::<i32>()?; // retorna mais cedo se a conversão falhar
Ok(number * 2)
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("oops"));
}

Saída:

Ok(42)
Err(ParseIntError { kind: InvalidDigit })
▶ Experimente você mesmo

Sem o ?, essa linha precisaria de um match só para tirar o number de dentro ou sair mais cedo com o erro. O ? faz exatamente isso com um único caractere. Ele só funciona dentro de uma função que também devolve um Result (ou um Option), porque é isso que dá a ele um lugar para onde devolver o erro.

O Rust também tem o panic!, que para o programa na hora. Ele é explicado com mais detalhes na próxima página. Como regra geral: use Result para falhas que quem chama talvez queira tratar (uma entrada inválida, um arquivo que não existe); deixe o panic! para situações que indicam um bug de verdade, em que continuar seria pior do que parar.

Quando as suas funções passam a falhar de vários jeitos diferentes, um erro do tipo String simples deixa de ser suficiente. A página de Erros personalizados mostra como definir o seu próprio tipo de erro, e a de anyhow e thiserror mostra os crates que a maioria dos projetos Rust usa para reduzir o código repetitivo.

Escreva fn parse_age(input: &str) -> Result<u32, String>. Ela deve devolver Err com uma mensagem útil se o texto não for um número ou se o número passar de 150. Teste com "34", "abc" e "200". Dica: faça match em input.parse::<u32>() e, dentro do braço Ok, use um if para conferir o tamanho.

Ver solução
fn parse_age(input: &str) -> Result<u32, String> {
match input.parse::<u32>() {
Ok(age) => {
if age > 150 {
Err(format!("{age} é velho demais para ser real"))
} else {
Ok(age)
}
}
Err(_) => Err(format!("'{input}' não é um número")),
}
}
fn main() {
for input in ["34", "abc", "200"] {
match parse_age(input) {
Ok(age) => println!("Idade: {age}"),
Err(e) => println!("Erro: {e}"),
}
}
}

Saída:

Idade: 34
Erro: 'abc' não é um número
Erro: 200 é velho demais para ser real
▶ Experimente você mesmo

Escreva fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>, que converte as duas strings em números e devolve a soma. Use ? em vez de match. Imprima o resultado para ("10", "20") e ("10", "vinte"). Dica: você precisa de um ? para cada parse, e a última linha embrulha a soma em Ok(...).

Ver solução
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
Ok(x + y)
}
fn main() {
println!("{:?}", add_strings("10", "20"));
println!("{:?}", add_strings("10", "vinte"));
}

Saída:

Ok(30)
Err(ParseIntError { kind: InvalidDigit })
▶ Experimente você mesmo

Err(ParseIntError { kind: InvalidDigit }) é como o Rust mostra o erro com {:?}: "vinte" contém um dígito inválido (InvalidDigit), e o ? devolveu esse erro na hora, sem chegar a fazer a soma.

  • Result<T, E> representa uma operação que pode dar certo (Ok) ou falhar (Err), com um valor de erro.
  • match trata os dois casos de forma explícita, com o mesmo padrão usado no Option.
  • O operador ? devolve um Err mais cedo de forma automática, então você não precisa de um match depois de cada chamada que pode falhar.
  • Prefira Result a panic! para erros dos quais quem chama pode se recuperar.

Teste o que aprendeu

1. Quais são as duas variantes de Result<T, E>?

2. O que o operador ? faz dentro de uma função que devolve um Result?

3. Segundo esta página, quando você deve preferir Result a panic!?

4. No exemplo de divide desta página, o que divide(10.0, 0.0) devolve?

5. Por que o operador ? só funciona dentro de uma função que também devolve um Result (ou Option)?

Pontuação: 0 / 5