O tipo Option
Nesta aula, exploramos o tipo Option em Rust, que substitui o uso de null. Aprendemos sobre as variantes Some e None, os principais métodos como unwrap, map e and_then, e como evitar erros comuns. Também discutimos boas práticas para usar Option de forma segura e idiomática.
Em Rust, não existe o conceito de null como em muitas outras linguagens. Em vez disso, a linguagem oferece o tipo Option<T>, que representa a possibilidade de um valor estar presente (Some(T)) ou ausente (None). Isso força o programador a tratar explicitamente os casos de ausência de valor, eliminando uma enorme classe de bugs comuns, como o temido null pointer exception. Nesta aula, vamos explorar o tipo Option em profundidade, desde sua definição básica até métodos avançados e boas práticas.
O tipo Option é um enum definido na biblioteca padrão como:
enum Option<T> {
None,
Some(T),
}
Ele é tão fundamental que está incluso no prelúdio (prelude) do Rust, ou seja, você pode usá-lo sem precisar importá-lo explicitamente. Vamos ver como utilizá-lo no dia a dia.
Some e None
As duas variantes de Option são Some e None. Some envolve um valor de qualquer tipo, enquanto None representa a ausência de valor. Para criar um Option, você pode usar diretamente os construtores:
let presente: Option<i32> = Some(42);
let ausente: Option<i32> = None;
O tipo de None precisa ser anotado ou inferido pelo contexto, pois o compilador precisa saber qual tipo T está faltando. Em muitos casos, a inferência funciona automaticamente.
Para extrair o valor de um Option, você pode usar match ou um dos métodos que veremos a seguir. O uso de match é a forma mais segura e explícita:
fn dividir(numerador: f64, denominador: f64) -> Option<f64> {
if denominador == 0.0 {
None
} else {
Some(numerador / denominador)
}
}
fn main() {
let resultado = dividir(10.0, 2.0);
match resultado {
Some(valor) => println!("Resultado: {}", valor),
None => println!("Erro: divisão por zero"),
}
}
Métodos (unwrap, map, and_then)
Rust fornece vários métodos convenientes para trabalhar com Option. Vamos explorar os mais importantes.
unwrap: Retorna o valor interno se for Some, ou causa um pânico (crash) se for None. Use com cuidado, apenas quando você tem certeza absoluta de que o valor existe.
let x: Option<i32> = Some(10);
let y = x.unwrap(); // y = 10
let z: Option<i32> = None;
// let w = z.unwrap(); // Isso causaria pânico!
map: Aplica uma função ao valor interno, se presente, e retorna um novo Option com o resultado. Se for None, retorna None.
let x = Some(5);
let y = x.map(|n| n * 2); // y = Some(10)
let z: Option<i32> = None;
let w = z.map(|n| n * 2); // w = None
and_then: Similar a map, mas a função fornecida deve retornar um Option. Útil para encadear operações que podem falhar.
fn metade(n: i32) -> Option<i32> {
if n % 2 == 0 {
Some(n / 2)
} else {
None
}
}
let x = Some(10);
let y = x.and_then(metade); // y = Some(5)
let z = Some(3);
let w = z.and_then(metade); // w = None
Outros métodos úteis incluem unwrap_or, unwrap_or_else, is_some, is_none, as_ref, as_mut, take, replace, etc.
Evitando null
Em linguagens com null, qualquer referência pode ser nula, e o programador precisa lembrar de verificar antes de usar. Em Rust, a ausência de valor é explicitamente representada por Option, e o compilador obriga você a tratar ambos os casos. Isso elimina uma categoria inteira de bugs.
Por exemplo, considere uma função que busca um usuário por ID. Em Java, você poderia retornar null se não encontrar. Em Rust, você retorna Option<Usuario>:
struct Usuario {
id: u32,
nome: String,
}
fn buscar_usuario(id: u32) -> Option<Usuario> {
// Simulação: retorna Some se id == 1, senão None
if id == 1 {
Some(Usuario {
id: 1,
nome: String::from("Alice"),
})
} else {
None
}
}
fn main() {
let usuario = buscar_usuario(1);
match usuario {
Some(u) => println!("Nome: {}", u.nome),
None => println!("Usuário não encontrado"),
}
}
Dessa forma, quem chama a função é forçado a considerar a possibilidade de falha. Não há como esquecer de verificar, pois o compilador não deixará você usar o valor sem tratá-lo.
Boas práticas
Ao trabalhar com Option, siga estas recomendações:
- Prefira
matchou métodos comomapeand_thenem vez deunwrap.unwrapsó deve ser usado em protótipos ou quando você tem certeza absoluta de que o valor nunca seráNone(por exemplo, em testes). - Use
unwrap_orpara fornecer um valor padrão quando a ausência é esperada. Por exemplo:let valor = opt.unwrap_or(0); - Evite aninhar
matchcom muitos níveis. Prefira encadeamento comand_thenou use?(operador de propagação de erro) quando estiver em funções que retornamOption. - Use
as_refeas_mutpara obter referências ao valor interno sem consumir oOption. Isso é útil quando você só precisa ler ou modificar o valor sem movê-lo. - Considere usar
Optionem vez de sentinelas como -1 ou strings vazias para representar ausência de valor. Isso torna o código mais expressivo e seguro.
Exemplo de uso de ? para propagar None:
fn encontrar_par_maior(v: Vec<i32>) -> Option<i32> {
let primeiro = v.first()?; // se vazio, retorna None
let segundo = v.get(1)?; // se não houver segundo, retorna None
if primeiro % 2 == 0 && segundo % 2 == 0 {
Some(*primeiro.max(segundo))
} else {
None
}
}
Referências
- Documentação oficial do Option
- The Rust Programming Language - Enums e Option
- Rust by Example - Option e unwrap
- Método map na documentação
- Método and_then na documentação
- Método unwrap na documentação
Exercícios
-
Crie uma função que recebe uma string e retorna
Option<usize>com o comprimento da string, mas retornaNonese a string estiver vazia.✓ Resposta:fn comprimento_se_nao_vazio(s: &str) -> Option<usize> { if s.is_empty() { None } else { Some(s.len()) } } -
Usando
map, escreva uma expressão que dobre o valor de umOption<i32>se ele existir, e retorneNonecaso contrário.✓ Resposta:let opt = Some(5); let dobrado = opt.map(|x| x * 2); // Some(10) let opt_none: Option<i32> = None; let dobrado_none = opt_none.map(|x| x * 2); // None -
Implemente uma função
raiz_quadradaque retornaOption<f64>com a raiz quadrada do número, mas retornaNonese o número for negativo.✓ Resposta:fn raiz_quadrada(x: f64) -> Option<f64> { if x < 0.0 { None } else { Some(x.sqrt()) } } -
Usando
and_then, encadeie duas funções que retornamOption: primeiro tente obter o primeiro caractere de uma string (pode falhar se a string for vazia), depois tente convertê-lo para maiúsculo (pode falhar se não for uma letra). Escreva o código completo.✓ Resposta:fn primeiro_char(s: &str) -> Option<char> { s.chars().next() } fn para_maiusculo(c: char) -> Option<char> { if c.is_alphabetic() { Some(c.to_ascii_uppercase()) } else { None } } fn main() { let s = "rust"; let resultado = primeiro_char(s).and_then(para_maiusculo); println!("{:?}", resultado); // Some('R') let vazia = ""; let resultado2 = primeiro_char(vazia).and_then(para_maiusculo); println!("{:?}", resultado2); // None let num = "123"; let resultado3 = primeiro_char(num).and_then(para_maiusculo); println!("{:?}", resultado3); // None } -
Refatore o código a seguir para usar
Optione evitar o uso deunwrapdesnecessário. O código atual usaunwrapem uma situação onde o valor pode serNone. Corrija-o de forma segura.fn main() { let numeros = vec![1, 2, 3]; let primeiro = numeros.first().unwrap(); println!("{}", primeiro); }✓ Resposta:O código original assume que o vetor não está vazio. Uma versão segura usa
matchouif let:fn main() { let numeros = vec![1, 2, 3]; match numeros.first() { Some(primeiro) => println!("{}", primeiro), None => println!("Vetor vazio"), } } // Ou usando if let: // if let Some(primeiro) = numeros.first() { // println!("{}", primeiro); // } else { // println!("Vetor vazio"); // }