Why this matters
A struct says "a value has this AND that". Very often data is "this OR that": a payment is by card or by bank transfer or in cash; a shape is a circle or a rectangle; a network message is a login, a chat line or a logout, each carrying different data. Rust's enums model "one of several alternatives", each alternative optionally with its own data, and match handles them with compile-time checking that every case is covered. Enums are arguably Rust's most powerful everyday feature; Option and Result (next module) are enums.
Simple enums
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
North,
East,
South,
West,
}
fn turn_right(d: Direction) -> Direction {
match d {
Direction::North => Direction::East,
Direction::East => Direction::South,
Direction::South => Direction::West,
Direction::West => Direction::North,
}
}
fn main() {
let mut d = Direction::North;
for _ in 0..5 {
print!("{:?} ", d);
d = turn_right(d);
}
println!();
println!("{}", d == Direction::East);
}
North East South West North
true
enum Name { Variant, ... }defines a type whose values are exactly one of the listed variants.- Variants are named with the type as prefix:
Direction::North. matchon an enum lists the variants; it must cover all of them (E0004 otherwise).#[derive(...)]asks for generated code:Debugfor{:?},Clone, Copyso the value is copied like an integer,PartialEqfor==. Module 10 explains these traits.
Variants with data
Each variant can carry its own data, in tuple form or struct form:
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
Color(u8, u8, u8),
}
fn describe(m: &Message) -> String {
match m {
Message::Quit => String::from("quit"),
Message::Move { x, y } => format!("move to ({x}, {y})"),
Message::Write(text) => format!("write {text:?}"),
Message::Color(r, g, b) => format!("color #{r:02x}{g:02x}{b:02x}"),
}
}
fn main() {
let msgs = [
Message::Move { x: 3, y: -1 },
Message::Write(String::from("hello")),
Message::Color(255, 128, 0),
Message::Quit,
];
for m in &msgs {
println!("{}", describe(m));
}
}
move to (3, -1)
write "hello"
color #ff8000
quit
The patterns in match destructure each variant, binding its fields to names (x, y, text). Because we matched on a reference (m: &Message), the bindings are references too (text is a &String): matching borrows instead of moving. A Message value is always exactly one of the four variants, and only that variant's fields exist; there is no way to read x from a Write message.
Compare the alternative without enums: a struct with a kind field and every possible field, most of them meaningless for any given kind, and nothing stopping code from reading text on a Move. Enums make such invalid states unrepresentable.
Methods on enums
impl works on enums exactly as on structs:
enum Shape {
Circle(f64),
Square(f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => 3.14159 * r * r,
Shape::Square(s) => s * s,
}
}
fn name(&self) -> &'static str {
match self {
Shape::Circle(_) => "circle",
Shape::Square(_) => "square",
}
}
}
fn main() {
let shapes = [Shape::Circle(1.0), Shape::Square(2.0)];
let mut total = 0.0;
for s in &shapes {
println!("{} {:.2}", s.name(), s.area());
total += s.area();
}
println!("total {:.2}", total);
}
circle 3.14
square 4.00
total 7.14
How an enum is stored
An enum value is a tagged union: a small integer tag (the discriminant) saying which variant it is, plus enough space for the largest variant's data. All variants share the same space.
enum Message { Quit, Move { x: i32, y: i32 }, Color(u8, u8, u8) }
tag payload (8 bytes, shared)
+----+---------------------------+
| 0 | (unused) | Quit
+----+---------------------------+
| 1 | x (4 bytes) | y (4 bytes) | Move
+----+---------------------------+
| 2 | r | g | b | (unused) | Color
+----+---------------------------+
use std::mem::size_of;
#[allow(dead_code)]
enum Small { Quit, Move { x: i32, y: i32 }, Color(u8, u8, u8) }
#[allow(dead_code)]
enum WithString { Quit, Write(String) }
fn main() {
println!("Small: {}", size_of::<Small>());
println!("WithString: {}", size_of::<WithString>());
println!("String: {}", size_of::<String>());
}
Small: 12
WithString: 24
String: 24
Small needs 8 bytes of payload plus a tag, padded to 12. WithString is the same size as a String: the compiler found bit patterns a String can never contain (its capacity can never exceed isize::MAX) and uses one of them to mean Quit. This niche optimization is why Option<&T> and Option<Box<T>> cost nothing extra, as you will see next module.
A match compiles to a check of the tag followed by a jump, often through a jump table: as fast as a C switch.
C-like enums and discriminants
Enums without data can be given explicit integer values and converted with as:
#[derive(Debug, Clone, Copy)]
enum HttpStatus {
Ok = 200,
NotFound = 404,
ServerError = 500,
}
fn main() {
let s = HttpStatus::NotFound;
println!("{:?} = {}", s, s as u16);
println!("{}", HttpStatus::Ok as i32 + 1);
}
NotFound = 404
201
The reverse conversion (integer to enum) is not automatic, because most integers are not valid variants; you write a match for it, returning an Option for unknown values (next module).
Common mistakes
Non-exhaustive match: adding a variant breaks every match without a catch-all, which is the point:
enum Light { Red, Yellow, Green }
fn main() {
let l = Light::Green;
let go = match l {
Light::Red => false,
Light::Yellow => false,
};
println!("{go}");
}
error[E0004]: non-exhaustive patterns: `Light::Green` not covered
--> main.rs:5:20
|
5 | let go = match l {
| ^ pattern `Light::Green` not covered
|
note: `Light` defined here
--> main.rs:1:6
|
1 | enum Light { Red, Yellow, Green }
| ^^^^^ ----- not covered
= note: the matched value is of type `Light`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
7 ~ Light::Yellow => false,
8 ~ Light::Green => todo!(),
|
Forgetting the type prefix: writing Red instead of Light::Red in a pattern does not refer to the variant; it creates a catch-all binding named Red (rustc warns about this: "pattern binding Red is named the same as one of the variants"). Always write Light::Red, or bring variants into scope with use Light::*;.
Comparing enums with == without PartialEq: E0369 "binary operation == cannot be applied". Derive PartialEq, or use match / matches!(value, Light::Red).
Moving data out of a borrowed enum: matching &Message binds references; trying to move the String out of a borrowed enum is E0507. Match on the owned value (match m { ... } where m: Message) to take it.
Recap
enumdefines a type whose values are exactly one of several variants.- Variants can carry data, tuple-style
Write(String)or struct-styleMove { x, y }. matchdestructures variants, must be exhaustive, and borrows when matching on a reference.- Enums are tagged unions: a tag plus space for the largest variant; niches can hide the tag.
- Methods go in
implblocks; C-like enums can have explicit discriminants and convert withas.
Going further: model a playing card with enum Suit and enum Rank inside a struct Card, and write fn beats(&self, other: &Card) -> bool. Then print size_of::<Option<String>>() and compare it with size_of::<String>().