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 unwrap em produção: Use expect com uma mensagem descritiva ou trate o caso None explicitamente.
  • Prefira map e and_then a match quando a lógica for simples: Isso torna o código mais conciso e expressivo.
  • Use o operador ? para propagar None: Em funções que retornam Option, o ? simplifica o encadeamento.
  • Considere Option para valores que podem não existir, não para erros: Para erros, use Result com um tipo de erro apropriado.
  • Use as_ref() ou as_mut() para evitar mover o valor: Se você só precisa de uma referência, use as_ref() para obter Option<&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

Exercícios

  1. Crie uma função que receba um Option<i32> e retorne o dobro do valor se for Some, ou None caso contrário. Use map para 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
    }
    
  2. Escreva uma função que receba dois Option<i32> e retorne a soma deles como Option<i32>. Se algum for None, retorne None. Use and_then ou zip e map.

    ✓ 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
    }
    
  3. Implemente uma função que receba uma string e tente convertê-la para um número inteiro. Retorne Option<i32>. Use o método parse.

    ✓ Resposta:
    fn parse_int(s: &str) -> Option<i32> {
        s.parse().ok()
    }
    
    fn main() {
        println!("{:?}", parse_int("42"));   // Some(42)
        println!("{:?}", parse_int("abc"));  // None
    }
    
  4. Dado um Option<String>, escreva uma função que retorne o comprimento da string se presente, ou 0 caso contrário. Use map e unwrap_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
    }
    
  5. Crie uma função que receba um Option<Vec<i32>> e retorne o primeiro elemento se existir, ou None. Use and_then e o método first do vetor (que já retorna Option).

    ✓ 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
    }