Por que você está cansado de escrever macros para Option em Result?
Você programa em Rust e, de repente, percebe: você tem um Option<T>, mas precisa de um Result<T, E>. Parece um detalhe. Mas se você é iniciante, o primeiro pensamento é: "Provavelmente preciso de uma macro!" Ou você já é um desenvolvedor experiente, mas toda vez escreve a mesma coisa: match option { Some(v) => Ok(v), None => Err("error") }. Concordo, isso é cansativo.
A pergunta no Stack Overflow "Existe uma maneira de simplificar a conversão de um Option em um Result sem uma macro?" acumulou milhares de visualizações. Não é por acaso: o problema é familiar para todos que trabalham com Rust. Mas a boa notícia é que a solução existe e é surpreendentemente simples. Neste tutorial, abordaremos todas as formas, desde as básicas até as elegantes, sem uma única macro.
Não vamos apenas dar a resposta — vamos mostrar como pensar como um desenvolvedor Rust. Vamos lá!
O que são Option e Result em Rust?
Antes de converter, vamos garantir que estamos na mesma página.
- Option<T> — é um tipo que pode ser
Some(T)(valor existe) ouNone(valor não existe). É usado quando a ausência de valor é uma situação normal (por exemplo, buscar um elemento em uma coleção). - Result<T, E> — é um tipo que pode ser
Ok(T)(tudo certo) ouErr(E)(ocorreu um erro). É usado para operações que podem falhar (por exemplo, ler um arquivo).
Problema: às vezes você tem um Option, mas pela lógica do programa, a ausência de valor é um erro. É aí que a conversão é necessária.
Forma básica: match (mas você já sabe disso)
A forma mais óbvia e frequentemente usada por iniciantes é o match. Sim, funciona, mas é verboso:
fn get_user_name(id: u32) -> Option<String> { // ... alguma lógica if id == 1 { Some("Alice".to_string()) } else { None }}
fn main() { let user_id = 2; let name = match get_user_name(user_id) { Some(name) => Ok(name), None => Err("User not found"), }; println!("{:?}", name); // Err("User not found")}Este código é correto, mas se houver dezenas dessas conversões no projeto, você se cansará de escrever match. Além disso, é fácil errar e esquecer de tratar o None. Precisamos de algo mais conciso.
Método ok_or: seu novo melhor amigo
A biblioteca padrão do Rust fornece o método ok_or para Option. Ele faz exatamente o que precisamos: converte Some(v) em Ok(v), e None em Err(error), onde error é o valor que você passa.
fn main() { let user_id = 2; let name = get_user_name(user_id).ok_or("User not found"); println!("{:?}", name); // Err("User not found")}Apenas uma linha! Mas há um detalhe: ok_or recebe um valor por posse (owned value). Se você passar um literal de string, como no exemplo, funciona, porque &str implementa Into<String>. Mas se você precisar passar um erro que já está armazenado em algum lugar, isso pode causar cópias desnecessárias.
Para esses casos, existe ok_or_else — ele recebe um closure que é chamado apenas se o Option for None. Isso economiza recursos:
fn main() { let user_id = 2; let error_msg = format!("User with id {} not found", user_id); let name = get_user_name(user_id).ok_or_else(|| error_msg.clone()); // ou ainda melhor: let name = get_user_name(user_id).ok_or_else(|| format!("User {} not found", user_id)); println!("{:?}", name);}Dica: use ok_or se a mensagem de erro for uma constante. Use ok_or_else se a mensagem de erro precisar ser calculada ou se você quiser evitar cópias desnecessárias.