Pular para o conteúdo

Enums em Rust

Um enum (abreviação de “enumeração”) define um tipo que pode ser uma entre um conjunto fixo de possibilidades. Se uma struct serve para dizer “esta coisa tem todos estes campos”, um enum serve para dizer “esta coisa é exatamente uma destas opções”.

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Indo para o norte"),
Direction::South => println!("Indo para o sul"),
Direction::East => println!("Indo para o leste"),
Direction::West => println!("Indo para o oeste"),
}
}

Saída:

Indo para o norte
▶ Experimente você mesmo

Repare que esse match não precisa de um _ para “pegar todo o resto”. Como todas as Direction possíveis estão listadas, o Rust já sabe que o match está completo.

É aqui que os enums do Rust vão além dos enums comuns de outras linguagens: cada variante pode carregar seus próprios dados:

enum Shape {
Circle(f64), // raio
Rectangle(f64, f64), // largura, altura
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

Saída:

28.27
20.00
▶ Experimente você mesmo

Shape::Circle carrega um f64 (o raio); Shape::Rectangle carrega dois. Em cada braço, o match já tira esses dados de dentro da variante para você, sem nenhum passo extra para “desembrulhar”.

Assim como uma struct, um enum pode ter um bloco impl com métodos. Dentro de um método, você normalmente faz match self para decidir o que fazer em cada variante:

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?} por {} segundos", light, light.seconds());
light = light.next();
}
}

Saída:

Red por 30 segundos
Green por 25 segundos
Yellow por 3 segundos
Red por 30 segundos
▶ Experimente você mesmo

next devolve um Light novinho, e light = light.next(); troca o antigo pelo novo. É por isso que light foi declarado como mut. O #[derive(Debug)] funciona em enums do mesmo jeito que em structs, então {:?} imprime o nome da variante (Red = vermelho, Yellow = amarelo, Green = verde).

Colocar a lógica em métodos deixa tudo perto do tipo. Quem usa um Light pode chamar light.seconds() sem saber como o tempo é decidido. E, se você adicionar uma variante nova depois, como FlashingYellow (amarelo piscante), o compilador aponta cada match que você precisa atualizar.

O tipo Option, que você vai conhecer direito na página dele, é só um enum definido na biblioteca padrão:

enum Option<T> {
Some(T),
None,
}

Depois que você entende enums, Option deixa de parecer uma sintaxe especial. É um enum totalmente comum, com duas variantes, e uma delas por acaso carrega um valor.

Por que não usar uma struct com um campo de “tipo”?

Seção intitulada “Por que não usar uma struct com um campo de “tipo”?”

Você poderia tentar imitar isso com uma struct e um campo de texto, como "circle" ou "rectangle". Só que aí nada impede você de digitar "circl" por engano, e você teria que verificar na mão quais campos realmente fazem sentido para cada tipo. Com um enum, combinações inválidas nem chegam a ser possíveis de escrever, e o compilador obriga você a tratar todas as variantes.

Crie um enum Coin (moeda) com as variantes Penny, Nickel, Dime e Quarter (as moedas americanas de 1, 5, 10 e 25 centavos). Escreva fn value_in_cents(coin: &Coin) -> u32 e depois some uma carteira de moedas: um quarter, um dime, um nickel, um penny e mais um quarter. Dica: guarde as moedas em um array e percorra &purse.

Ver solução
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("Total: {total} centavos");
}

Saída:

Total: 66 centavos
▶ Experimente você mesmo

Pegue o enum Shape desta página e adicione uma variante Triangle que guarda o comprimento dos três lados. Depois, escreva uma função perimeter (o perímetro, a distância em volta da forma toda) que funcione para todas as formas. Dica: o perímetro de um círculo é 2.0 * 3.14159 * radius. O de um triângulo é a soma dos três lados.

Ver solução
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

Saída:

6.28
18.00
12.00
▶ Experimente você mesmo

Experimente apagar o braço Shape::Triangle do match. O Rust se recusa a compilar, porque o match não cobre mais todas as variantes. É o compilador garantindo que você nunca esqueça um caso.

Crie um enum Temperature com duas variantes que guardam um f64 cada: Celsius e Fahrenheit. Adicione um método to_celsius(&self) -> f64 e imprima Celsius(25.0), Fahrenheit(212.0) e Fahrenheit(50.0) em °C, com uma casa decimal. Dica: para converter, use (f - 32.0) * 5.0 / 9.0.

Ver solução
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

Saída:

25.0 °C
100.0 °C
10.0 °C
▶ Experimente você mesmo

Como self é emprestado, c e f são referências (&f64). A conta funciona direto com referências, mas, para devolver o valor em Celsius puro, você escreve *c, que quer dizer “o número para o qual c aponta”.

  • Um enum define um tipo como uma entre várias variantes com nome: enum Direction { North, South, ... }.
  • As variantes podem carregar seus próprios dados, e cada variante pode carregar dados diferentes.
  • O match desestrutura os dados do enum de forma natural e exige que todas as variantes sejam tratadas.
  • Enums podem ter métodos em um bloco impl, assim como structs.
  • Option (e Result, que você verá mais adiante) são só enums da biblioteca padrão. Não têm nada de mágico.

Teste o que aprendeu

1. O que cada variante de um enum em Rust pode fazer, ao contrário de um enum comum em outras linguagens?

2. Por que o match do exemplo com Direction não precisa de um braço _ para o resto?

3. Como Option<T> é implementado de fato na biblioteca padrão?

Pontuação: 0 / 3