Enums Exercises

Overview

These exercises introduce Rust’s powerful enum type — far more capable than enums in most languages.


enums1 - Basic Enum Definition

Concept: Defining an enum with variants

#[derive(Debug)]
enum Message {
    Resize,
    Move,
    Echo,
    ChangeColor,
    Quit,
}
 
fn main() {
    println!("{:?}", Message::Resize);
    println!("{:?}", Message::Move);
}

Key Takeaways:

  • Enums define a type by enumerating its possible variants
  • Access variants with EnumName::VariantName
  • #[derive(Debug)] allows printing with {:?}

enums2 - Enums with Data

Concept: Variants can hold different types of data

#[derive(Debug)]
struct Point { x: u64, y: u64 }
 
#[derive(Debug)]
enum Message {
    Resize { width: u64, height: u64 },  // Struct-like variant
    Move(Point),                          // Tuple-like with struct
    Echo(String),                         // Tuple-like with String
    ChangeColor(u8, u8, u8),              // Tuple-like with multiple values
    Quit,                                 // Unit variant (no data)
}
 
impl Message {
    fn call(&self) {
        println!("{self:?}");
    }
}

Variant Types:

SyntaxNameExample
Variant { field: Type }Struct-likeResize { width: 10, height: 30 }
Variant(Type)Tuple-likeMove(Point { x: 10, y: 15 })
Variant(T1, T2)Multi-value tupleChangeColor(200, 255, 255)
VariantUnitQuit

Usage example:

let messages = [
    Message::Resize { width: 10, height: 30 },
    Message::Move(Point { x: 10, y: 15 }),
    Message::Echo(String::from("hello world")),
    Message::ChangeColor(200, 255, 255),
    Message::Quit,
];
 
for message in &messages {
    message.call();
}

Why Enums Are Powerful

Unlike C enums (just integers), Rust enums can:

  • Hold different data in each variant
  • Implement methods
  • Be used with pattern matching (coming soon!)

🔮 Preview: match expressions let you handle each variant differently. You’ll learn this in the next set of exercises!


Common Standard Library Enums

enum Option<T> {
    Some(T),
    None,
}
 
enum Result<T, E> {
    Ok(T),
    Err(E),
}

These are everywhere in Rust!


enums3 - Pattern Matching with match

Concept: Using match to handle enum variants and destructure their data

The Full Code

struct Point {
    x: u64,
    y: u64,
}
 
enum Message {
    Resize { width: u64, height: u64 },
    Move(Point),
    Echo(String),
    ChangeColor(u8, u8, u8),
    Quit,
}
 
struct State {
    width: u64,
    height: u64,
    position: Point,
    message: String,
    color: (u8, u8, u8),
    quit: bool,
}
 
impl State {
    fn resize(&mut self, width: u64, height: u64) {
        self.width = width;
        self.height = height;
    }
 
    fn move_position(&mut self, point: Point) {
        self.position = point;
    }
 
    fn echo(&mut self, s: String) {
        self.message = s;
    }
 
    fn change_color(&mut self, red: u8, green: u8, blue: u8) {
        self.color = (red, green, blue);
    }
 
    fn quit(&mut self) {
        self.quit = true;
    }
 
    fn process(&mut self, message: Message) {
        match message {
            Message::Resize { width, height } => self.resize(width, height),
            Message::Move(point) => self.move_position(point),
            Message::Echo(string) => self.echo(string),
            Message::ChangeColor(red, green, blue) => self.change_color(red, green, blue),
            Message::Quit => self.quit(),
        }
    }
}

Where is Destructuring Happening?

Destructuring happens in the match arm patterns (the left side of =>):

match message {
    Message::Resize { width, height } => ...
    //              ^^^^^^^^^^^^^^^^
    //              DESTRUCTURING! Extracts `width` and `height` from the variant
    
    Message::Move(point) => ...
    //           ^^^^^^^
    //           DESTRUCTURING! Extracts the Point into `point`
    
    Message::ChangeColor(red, green, blue) => ...
    //                  ^^^^^^^^^^^^^^^^^
    //                  DESTRUCTURING! Extracts all 3 u8 values
    
    Message::Quit => ...
    //            No data to destructure (unit variant)
}

Destructuring = extracting inner values from a data structure into named variables.


Step-by-Step Code Walkthrough

Step 1: Create Initial State

let mut state = State {
    width: 0,
    height: 0,
    position: Point { x: 0, y: 0 },
    message: String::from("hello world"),
    color: (0, 0, 0),
    quit: false,
};

State starts with default values. It’s mut because process() will modify it.


Step 2: Process a Resize Message

state.process(Message::Resize { width: 10, height: 30 });

What happens inside process():

  1. message is Message::Resize { width: 10, height: 30 }
  2. match checks each arm from top to bottom
  3. First arm matches: Message::Resize { width, height }
  4. Destructuring: width becomes 10, height becomes 30
  5. Calls self.resize(10, 30)
  6. State is now: { width: 10, height: 30, ... }

Step 3: Process a Move Message

state.process(Message::Move(Point { x: 10, y: 15 }));

What happens inside process():

  1. message is Message::Move(Point { x: 10, y: 15 })
  2. Second arm matches: Message::Move(point)
  3. Destructuring: the entire Point { x: 10, y: 15 } is bound to point
  4. Calls self.move_position(point)
  5. State is now: { position: Point { x: 10, y: 15 }, ... }

Step 4: Process an Echo Message

state.process(Message::Echo(String::from("Hello world!")));

What happens inside process():

  1. message is Message::Echo(String::from("Hello world!"))
  2. Third arm matches: Message::Echo(string)
  3. Destructuring: the String is bound to string
  4. Calls self.echo(string) — ownership of the String moves!
  5. State is now: { message: "Hello world!", ... }

Step 5: Process a ChangeColor Message

state.process(Message::ChangeColor(255, 0, 255));

What happens inside process():

  1. message is Message::ChangeColor(255, 0, 255)
  2. Fourth arm matches: Message::ChangeColor(red, green, blue)
  3. Destructuring: red = 255, green = 0, blue = 255
  4. Calls self.change_color(255, 0, 255)
  5. State is now: { color: (255, 0, 255), ... }

Step 6: Process a Quit Message

state.process(Message::Quit);

What happens inside process():

  1. message is Message::Quit
  2. Fifth arm matches: Message::Quit
  3. No destructuring needed (no data in this variant)
  4. Calls self.quit()
  5. State is now: { quit: true, ... }

Destructuring Patterns Summary

Variant DefinitionMatch PatternWhat Gets Extracted
Resize { width: u64, height: u64 }Resize { width, height }Named fields → variables
Move(Point)Move(point)Single value → point
Echo(String)Echo(string)Single value → string
ChangeColor(u8, u8, u8)ChangeColor(r, g, b)Three values → three variables
QuitQuitNothing (unit variant)

Key Takeaways

  1. match is exhaustive — you must handle every variant or use _ as a catch-all
  2. Destructuring happens in patterns — the left side of =>
  3. Variable names are up to youMessage::Move(p) works just as well as Message::Move(point)
  4. Ownership transfers — when you match Echo(string), the String is moved into string
  5. The compiler prevents mistakes — forget a variant? Compiler error!

🎯 This is one of Rust’s superpowers! Pattern matching + enums = safe, expressive code that handles all cases.


Rust Book Reference