O tipo Option
Nesta aula, exploramos o tipo Option do Rust, que substitui o conceito de null de forma segura. Aprendemos a usar Some e None, métodos como unwrap, map e and_then, e boas práticas para evitar erros em tempo de execução.
O tipo Option é uma das ferramentas mais fundamentais do Rust para lidar com a ausência de valor. Diferente de linguagens que usam null ou nil, o Rust encapsula a possibilidade de ausência em um tipo enum, forçando o programador a tratar ambos os casos de forma explícita. Isso elimina uma classe inteira de bugs comuns, como a temida NullPointerException.
Nesta aula, vamos nos aprofundar nos dois variantes do Option, nos métodos mais importantes e nas práticas recomendadas para escrever código robusto e idiomático em Rust.
Some e None
O tipo Option<T> é definido como um enum com duas variantes: Some(T) e None. Some(T) indica que um valor do tipo T está presente, enquanto None indica ausência de valor. Essa representação é segura porque o compilador exige que você trate explicitamente o caso None antes de usar o valor interno.
Por exemplo, ao dividir dois números inteiros, o resultado pode não ser inteiro. Em vez de retornar um valor sentinela ou lançar exceção, você pode retornar Option<i32>:
fn divisao_segura(dividendo: i32, divisor: i32) -> Option<i32> {
if divisor == 0 {
None
} else {
Some(dividendo / divisor)
}
}
fn main() {
let resultado = divisao_segura(10, 2);
match resultado {
Some(valor) => println!("Resultado: {}", valor),
None => println!("Erro: divisão por zero"),
}
}
O uso de match é a forma mais direta de extrair o valor de um Option, mas existem métodos mais concisos para muitas situações comuns.
Métodos (unwrap, map, and_then)
O Rust fornece vários métodos embutidos no Option para facilitar o trabalho com valores opcionais. Os mais importantes são unwrap, map e and_then.
unwrap
O método unwrap extrai o valor de um Some, mas causa um panic! se o valor for None. Ele é útil quando você tem certeza de que o valor não é None (por exemplo, em protótipos ou testes). Em código de produção, prefira expect ou tratamento explícito.
let x: Option<i32> = Some(42);
println!("{}", x.unwrap()); // 42
let y: Option<i32> = None;
// y.unwrap(); // panic!
map
O método map transforma o valor dentro de um Some usando uma closure, retornando um novo Option. Se for None, retorna None sem executar a closure.
let numero: Option<i32> = Some(5);
let dobrado = numero.map(|n| n * 2);
println!("{:?}", dobrado); // Some(10)
let nulo: Option<i32> = None;
let ainda_nulo = nulo.map(|n| n * 2);
println!("{:?}", ainda_nulo); // None
and_then
O método and_then (também conhecido como flat_map) é usado quando a própria closure retorna um Option. Ele é útil para encadear operações que podem falhar.
fn raiz_quadrada(x: f64) -> Option<f64> {
if x < 0.0 { None } else { Some(x.sqrt()) }
}
let numero: Option<f64> = Some(16.0);
let resultado = numero.and_then(raiz_quadrada);
println!("{:?}", resultado); // Some(4.0)
let negativo: Option<f64> = Some(-1.0);
let resultado2 = negativo.and_then(raiz_quadrada);
println!("{:?}", resultado2); // None
Outros métodos úteis incluem unwrap_or, unwrap_or_else, is_some, is_none, as_ref, take, entre outros.
Evitando null
Em linguagens como Java, C# ou JavaScript, o uso de null é uma fonte frequente de erros. O Rust elimina esse problema ao não ter null como conceito nativo. Em vez disso, sempre que um valor pode estar ausente, usamos Option. Isso torna o código mais seguro e auto-documentado: a assinatura de uma função que retorna Option<T> já informa ao chamador que o resultado pode não existir.
Por exemplo, ao buscar um elemento em um vetor pelo índice, o método get retorna Option<&T>:
let v = vec![1, 2, 3];
let valor = v.get(2); // Some(&3)
let fora = v.get(5); // None
Se você tentar acessar diretamente com v[5], o Rust causaria um panic!. Usando get, você é forçado a lidar com a possibilidade de ausência.
Outra vantagem é que o Option pode ser combinado com o operador ? (ponto de interrogação) para propagar erros de forma concisa em funções que retornam Result ou Option. Por exemplo:
fn ler_arquivo(nome: &str) -> Option<String> {
let conteudo = std::fs::read_to_string(nome).ok()?;
Some(conteudo)
}
Aqui, read_to_string retorna um Result; o método ok() converte para Option e o ? retorna None se houver erro.
Boas práticas
Ao trabalhar com Option, siga estas recomendações para escrever código idiomático e seguro:
- Evite
unwrapem produção: Useexpectcom uma mensagem descritiva ou trate o casoNoneexplicitamente. - Prefira
mapeand_thenamatchquando a lógica for simples: Isso torna o código mais conciso e expressivo. - Use o operador
?para propagarNone: Em funções que retornamOption, o?simplifica o encadeamento. - Considere
Optionpara valores que podem não existir, não para erros: Para erros, useResultcom um tipo de erro apropriado. - Use
as_ref()ouas_mut()para evitar mover o valor: Se você só precisa de uma referência, useas_ref()para obterOption<&T>sem consumir o original.
Observações finais
Dominar o Option é essencial para programar em Rust. Ele é a base para muitos padrões de design, como encadeamento de operações opcionais e tratamento de ausência de forma segura. Pratique combinando Option com closures e o operador ? para escrever código elegante e robusto.
Referências
- Documentação oficial do Option
- Capítulo do livro sobre enums e Option
- Rust by Example: Option & unwrap
- Método map na documentação
- Método and_then na documentação
- Recoverable Errors with Result
Exercícios
Crie uma função que receba um
Option<i32>e retorne o dobro do valor se forSome, ouNonecaso contrário. Usemappara implementar.✓ Resposta:fn dobra(valor: Option<i32>) -> Option<i32> { valor.map(|v| v * 2) } fn main() { println!("{:?}", dobra(Some(5))); // Some(10) println!("{:?}", dobra(None)); // None }Escreva uma função que receba dois
Option<i32>e retorne a soma deles comoOption<i32>. Se algum forNone, retorneNone. Useand_thenouzipemap.✓ Resposta:fn soma_op(a: Option<i32>, b: Option<i32>) -> Option<i32> { a.zip(b).map(|(x, y)| x + y) } fn main() { println!("{:?}", soma_op(Some(3), Some(4))); // Some(7) println!("{:?}", soma_op(Some(3), None)); // None }Implemente uma função que receba uma string e tente convertê-la para um número inteiro. Retorne
Option<i32>. Use o métodoparse.✓ Resposta:fn parse_int(s: &str) -> Option<i32> { s.parse().ok() } fn main() { println!("{:?}", parse_int("42")); // Some(42) println!("{:?}", parse_int("abc")); // None }Dado um
Option<String>, escreva uma função que retorne o comprimento da string se presente, ou 0 caso contrário. Usemapeunwrap_or.✓ Resposta:fn comprimento_op(s: Option<String>) -> usize { s.map(|x| x.len()).unwrap_or(0) } fn main() { println!("{}", comprimento_op(Some("Rust".to_string()))); // 4 println!("{}", comprimento_op(None)); // 0 }Crie uma função que receba um
Option<Vec<i32>>e retorne o primeiro elemento se existir, ouNone. Useand_thene o métodofirstdo vetor (que já retornaOption).✓ Resposta:fn primeiro_elemento(v: Option<Vec<i32>>) -> Option<i32> { v.and_then(|vec| vec.first().copied()) } fn main() { let v = Some(vec![10, 20, 30]); println!("{:?}", primeiro_elemento(v)); // Some(10) println!("{:?}", primeiro_elemento(None)); // None }