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