Option: eliminando null
1. O problema do null e a filosofia de Rust
Em 1965, o cientista da computação Tony Hoare introduziu o conceito de referências nulas em sua linguagem ALGOL W. Décadas depois, ele próprio classificou essa invenção como seu "erro de um bilhão de dólares". O motivo? null é a fonte mais comum de falhas catastróficas em software: o temido NullPointerException em Java, NullReferenceException em C#, ou segmentation faults em C/C++.
Quando uma variável pode ser null, cada acesso a ela se torna uma potencial bomba-relógio. O programador precisa lembrar de verificar se o valor não é nulo antes de usá-lo — e qualquer esquecimento resulta em crash em tempo de execução.
Rust adota uma abordagem radicalmente diferente: não existe null como tipo nativo. Em vez disso, a linguagem oferece o tipo Option<T>, que torna a ausência de valor explícita no sistema de tipos. Isso significa que o compilador força o programador a tratar todos os casos de ausência antes mesmo do código executar.
2. Introdução ao Option<T>
Option<T> é um enum definido na biblioteca padrão:
enum Option<T> {
Some(T),
None,
}
Esta é a essência de um tipo soma (ou tagged union): Option pode ser exatamente um de dois estados — Some(T) contendo um valor do tipo T, ou None representando ausência.
A diferença fundamental para null é que, em Rust, a ausência é explicitamente tipada. Uma variável String nunca pode ser nula; se você precisa de uma string opcional, usa-se Option<String>. O compilador garante que você não conseguirá acessar o valor interno sem antes verificar se ele existe.
3. Construindo e inspecionando Option
Criar valores opcionais é simples:
let presente: Option<i32> = Some(42);
let ausente: Option<i32> = None;
let nome: Option<String> = Some("Alice".to_string());
let vazio: Option<String> = None;
Métodos básicos permitem inspecionar o estado:
let x: Option<i32> = Some(10);
assert_eq!(x.is_some(), true);
assert_eq!(x.is_none(), false);
// CUIDADO: unwrap() pode causar panic!
println!("{}", x.unwrap()); // 10
let y: Option<i32> = None;
// y.unwrap(); // Isso causaria panic!
unwrap() é útil em protótipos e testes, mas perigoso em produção. Prefira tratamento explícito.
4. Pattern matching com Option
A forma mais segura e expressiva de lidar com Option é usando match:
fn dividir(dividendo: f64, divisor: f64) -> Option<f64> {
if divisor == 0.0 {
None
} else {
Some(dividendo / divisor)
}
}
fn main() {
let resultado = dividir(10.0, 2.0);
match resultado {
Some(valor) => println!("Resultado: {}", valor),
None => println!("Erro: divisão por zero"),
}
// Combinando com outras funções
let lista = vec![1, 2, 3];
let primeiro = lista.first(); // Retorna Option<&i32>
match primeiro {
Some(&n) if n > 0 => println!("Primeiro positivo: {}", n),
Some(&n) => println!("Primeiro: {}", n),
None => println!("Lista vazia"),
}
}
5. Combinadores e métodos úteis de Option
Rust oferece uma rica coleção de métodos para transformar Option sem extrair manualmente o valor:
// map(): transforma o valor interno se existir
let idade: Option<i32> = Some(25);
let idade_como_string: Option<String> = idade.map(|i| i.to_string());
// and_then(): encadeia operações que retornam Option
let arquivo: Option<String> = Some("dados.txt".to_string());
let conteudo = arquivo.and_then(|nome| ler_arquivo(nome));
// unwrap_or(): valor padrão seguro
let valor: Option<i32> = None;
let seguro = valor.unwrap_or(0); // 0
// unwrap_or_else(): valor padrão com closure (lazy)
let calculado = valor.unwrap_or_else(|| {
println!("Calculando valor padrão...");
42
});
// ok_or(): converte para Result com erro personalizado
let opt: Option<i32> = None;
let res: Result<i32, String> = opt.ok_or("valor ausente".to_string());
// take() e replace(): manipulação mutável
let mut cache: Option<String> = Some("dados".to_string());
let removido = cache.take(); // cache agora é None
cache.replace("novos dados".to_string()); // cache volta a ser Some
6. if let e while let com Option
Para casos onde só um braço do match interessa, if let oferece sintaxe mais concisa:
let opcao: Option<i32> = Some(42);
// Em vez de:
match opcao {
Some(valor) => println!("Valor: {}", valor),
None => {},
}
// Use:
if let Some(valor) = opcao {
println!("Valor: {}", valor);
}
// while let é perfeito para iteradores
let mut pilha = vec![1, 2, 3];
while let Some(topo) = pilha.pop() {
println!("Pop: {}", topo);
}
// Saída: 3, 2, 1
7. Padrões e boas práticas com Option
Evite unwrap() em produção. Prefira tratamento explícito ou use expect() com mensagem descritiva:
// Ruim:
let valor = opcao.unwrap();
// Melhor:
let valor = opcao.expect("Configuração obrigatória ausente");
// Ideal:
let valor = match opcao {
Some(v) => v,
None => {
eprintln!("Erro: configuração ausente");
std::process::exit(1);
}
};
Use o operador ? para propagar None em funções que retornam Option:
fn buscar_usuario(id: u32) -> Option<String> {
// Simulação de busca em banco
if id == 1 {
Some("Alice".to_string())
} else {
None
}
}
fn obter_email_usuario(id: u32) -> Option<String> {
let nome = buscar_usuario(id)?; // Se None, retorna None imediatamente
Some(format!("{}@empresa.com", nome.to_lowercase()))
}
fn main() {
let email = obter_email_usuario(1);
println!("{:?}", email); // Some("alice@empresa.com")
let email2 = obter_email_usuario(2);
println!("{:?}", email2); // None
}
Combine Option com Result para erros mais descritivos:
fn processar(valor: Option<i32>) -> Result<i32, String> {
let v = valor.ok_or("Valor ausente".to_string())?;
if v < 0 {
Err("Valor negativo não permitido".to_string())
} else {
Ok(v * 2)
}
}
Exemplo completo: busca em coleção:
struct Produto {
id: u32,
nome: String,
preco: f64,
}
fn buscar_produto(produtos: &[Produto], id: u32) -> Option<&Produto> {
produtos.iter().find(|p| p.id == id)
}
fn main() {
let catalogo = vec![
Produto { id: 1, nome: "Teclado".into(), preco: 150.0 },
Produto { id: 2, nome: "Mouse".into(), preco: 80.0 },
];
let produto = buscar_produto(&catalogo, 1);
match produto {
Some(p) => println!("Produto: {} - R${:.2}", p.nome, p.preco),
None => println!("Produto não encontrado"),
}
}
8. Conclusão: por que Option é superior a null
Option<T> não é apenas uma alternativa a null — é uma revolução na segurança de tipos. Enquanto em Java ou C# você precisa lembrar de verificar null manualmente (e pode esquecer), Rust torna essa verificação obrigatória em tempo de compilação.
Benefícios concretos:
- Segurança garantida: sem NullPointerExceptions em produção
- Documentação viva: Option<T> na assinatura de uma função comunica imediatamente que o valor pode estar ausente
- Integração com pattern matching: tratamento elegante de todos os casos
- Composição com iteradores: filter_map(), flat_map() e outros combinadores funcionam perfeitamente
- Propagação com ?: código limpo e seguro sem aninhamento excessivo
Em linguagens com null, cada variável é uma potencial armadilha. Em Rust, Option transforma a ausência de valor em um cidadão de primeira classe, tratado com o respeito que merece. O "erro de um bilhão de dólares" simplesmente não existe no ecossistema Rust.
Referências
- The Rust Programming Language - Chapter 6: Enums and Pattern Matching — Capítulo oficial do livro que introduz
Optione pattern matching - Rust by Example - Option — Tutoriais práticos com exemplos interativos de
Option - Rust Reference - The Option Type — Documentação completa da API de
Optionna biblioteca padrão - Null References: The Billion Dollar Mistake - Tony Hoare — Palestra original de Tony Hoare sobre o problema do null
- Rust Design Patterns - Option combinators — Padrões de design e boas práticas para uso de
Optionem Rust