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 match ou métodos como map e and_then em vez de unwrap. unwrap só 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_or para fornecer um valor padrão quando a ausência é esperada. Por exemplo: let valor = opt.unwrap_or(0);
  • Evite aninhar match com muitos níveis. Prefira encadeamento com and_then ou use ? (operador de propagação de erro) quando estiver em funções que retornam Option.
  • Use as_ref e as_mut para obter referências ao valor interno sem consumir o Option. Isso é útil quando você só precisa ler ou modificar o valor sem movê-lo.
  • Considere usar Option em 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

Exercícios

  1. Crie uma função que recebe uma string e retorna Option<usize> com o comprimento da string, mas retorna None se a string estiver vazia.

    ✓ Resposta:
    fn comprimento_se_nao_vazio(s: &str) -> Option<usize> {
        if s.is_empty() {
            None
        } else {
            Some(s.len())
        }
    }
  2. Usando map, escreva uma expressão que dobre o valor de um Option<i32> se ele existir, e retorne None caso 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
  3. Implemente uma função raiz_quadrada que retorna Option<f64> com a raiz quadrada do número, mas retorna None se o número for negativo.

    ✓ Resposta:
    fn raiz_quadrada(x: f64) -> Option<f64> {
        if x < 0.0 {
            None
        } else {
            Some(x.sqrt())
        }
    }
  4. Usando and_then, encadeie duas funções que retornam Option: 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
    }
  5. Refatore o código a seguir para usar Option e evitar o uso de unwrap desnecessário. O código atual usa unwrap em uma situação onde o valor pode ser None. 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 match ou if 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");
    // }