Enums (Enumerações)

A palavra-chave enum permite a criação de um tipo que possui algumas variantes diferentes:

#[derive(Debug)]
enum Direction {
    Left,
    Right,
}

#[derive(Debug)]
enum PlayerMove {
    Pass,                        // Variante simples
    Run(Direction),              // Tuple variant
    Teleport { x: u32, y: u32 }, // Struct variant
}

fn main() {
    let m: PlayerMove = PlayerMove::Run(Direction::Left);
    println!("On this turn: {:?}", m);
}
This slide should take about 5 minutes.

Pontos Chave:

  • Enumerações permitem coletar um conjunto de valores em um tipo
  • Direction é um tipo com variantes. Existem dois valores de Direction: Direction::Left e Direction::Right.
  • PlayerMove é um tipo com três variantes. Além dos payloads, o Rust armazenará um discriminante para que ele saiba em tempo de execução qual variante está em um valor PlayerMove.
  • Este pode ser um bom momento para comparar structs e enums:
    • Em ambos, você pode ter uma versão simples sem campos (unit struct, ou estrutura unitária) ou uma com diferentes tipos de campo (variant payloads ou cargas de variante).
    • Você pode até mesmo implementar as diferentes variantes de uma enum com structs separadas, mas elas não seriam do mesmo tipo, como seriam se todas fossem definidas em uma enum.
  • O Rust usa espaço mínimo para armazenar o discriminante.
    • Se necessário, armazena um inteiro do menor tamanho necessário

    • If the allowed variant values do not cover all bit patterns, it will use invalid bit patterns to encode the discriminant (the “niche optimization”). For example, Option<&u8> stores either a pointer to an integer or NULL for the None variant.

    • É possível controlar o discriminante se necessário (p.ex., para compatibilidade com C):

      #[repr(u32)]
      enum Bar {
          A, // 0
          B = 10000,
          C, // 10001
      }
      
      fn main() {
          println!("A: {}", Bar::A as u32);
          println!("B: {}", Bar::B as u32);
          println!("C: {}", Bar::C as u32);
      }

      Sem repr, o tipo do discriminante usa 2 bytes, porque 10001 cabe em 2 bytes.

Mais para Explorar

O Rust tem várias otimizações que pode empregar para fazer com que as enums ocupem menos espaço.

  • Otimização de ponteiro nulo: para alguns tipos, o Rust garante que size_of::<T>() é igual a size_of::<Option<T>>().

    Código de exemplo caso queira mostrar como a representação em bits pode ser na prática. É importante apontar que o compilador não oferece nenhuma garantia a respeito dessa representação, portanto isso é completamente inseguro.

    use std::mem::transmute;
    
    macro_rules! dbg_bits {
        ($e:expr, $bit_type:ty) => {
            println!("- {}: {:#x}", stringify!($e), transmute::<_, $bit_type>($e));
        };
    }
    
    fn main() {
        unsafe {
            println!("bool:");
            dbg_bits!(false, u8);
            dbg_bits!(true, u8);
    
            println!("Option<bool>:");
            dbg_bits!(None::<bool>, u8);
            dbg_bits!(Some(false), u8);
            dbg_bits!(Some(true), u8);
    
            println!("Option<Option<bool>>:");
            dbg_bits!(Some(Some(false)), u8);
            dbg_bits!(Some(Some(true)), u8);
            dbg_bits!(Some(None::<bool>), u8);
            dbg_bits!(None::<Option<bool>>, u8);
    
            println!("Option<&i32>:");
            dbg_bits!(None::<&i32>, usize);
            dbg_bits!(Some(&0i32), usize);
        }
    }