Enums & match
Prerequisites: Methods & impl blocks
An enum (enumeration) defines a type that can be one of several variants. Where a struct says "this and that", an enum says "this or that".
Syntax
enum Direction {
North,
South,
East,
West,
}
enum Shape {
Circle(f64), // variant holding one value
Rectangle { width: f64, height: f64 }, // variant with named fields
}
let d = Direction::North;
let s = Shape::Circle(1.5);
Unlike enums in many languages, Rust's variants can carry data,
and each variant can hold different kinds of data. Enums can have
impl blocks and methods, just like structs.
match compares a value against a series of patterns and runs
the code for the first one that matches. It's an expression, so it can
produce a value. Matches are exhaustive: the compiler checks that
every possible variant is handled. Use _ as a catch-all pattern for
"anything else".
Option<T> is an enum from the standard library that replaces
null:
enum Option<T> {
Some(T), // there is a value
None, // there is no value
}
Because an Option<i32> is a different type from an i32, the
compiler makes you handle the None case before you can use the
value — you can't forget a null check. Option, Some, and None
are available without any use.
if let is shorthand for a match where you only care about one
pattern:
if let Some(n) = maybe_number {
println!("got {n}");
}
Example
use std::f64::consts::PI;
enum Shape {
Circle(f64),
Rectangle { width: f64, height: f64 },
Triangle(f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => PI * r * r,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(base, height) => 0.5 * base * height,
}
}
}
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 { None } else { Some(a / b) }
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle { width: 2.0, height: 3.0 },
Shape::Triangle(4.0, 5.0),
];
for shape in &shapes {
println!("area: {}", shape.area());
}
let number = 7;
let size = match number {
0 => "zero",
1..=9 => "small",
_ => "large",
};
println!("{number} is {size}");
match divide(10, 2) {
Some(result) => println!("10 / 2 = {result}"),
None => println!("cannot divide by zero"),
}
match divide(1, 0) {
Some(result) => println!("1 / 0 = {result}"),
None => println!("cannot divide by zero"),
}
if let Some(result) = divide(9, 3) {
println!("9 / 3 = {result}");
}
let missing = divide(5, 0);
println!("with default: {}", missing.unwrap_or(0));
}
unwrap_or(0) returns the value inside Some, or 0 for None.
Expected output
area: 3.141592653589793
area: 6
area: 10
7 is small
10 / 2 = 5
cannot divide by zero
9 / 3 = 3
with default: 0
Common mistakes
- Missing a variant in
match:
let name = match d {
Direction::North => "north",
Direction::South => "south",
Direction::East => "east",
};
error[E0004]: non-exhaustive patterns: `Direction::West` not covered
Add the missing arm, or a _ => arm if the remaining cases really
should share one behavior.
- Using an
Option<T>as if it were aT.let total = maybe + 1;wheremaybeis anOption<i32>:
error[E0369]: cannot add `{integer}` to `Option<i32>`
Get the value out first with match, if let, or unwrap_or.
- Reaching for
.unwrap()on anOption. It panics onNone; prefermatch,if let, orunwrap_orunless you're certain a value is present. - Forgetting the enum name: write
Direction::North, notNorth.
Practice
Define an enum Coin with the variants Penny, Nickel, Dime, and
Quarter, and a function value_in_cents(coin: &Coin) -> u32 that
uses match to return 1, 5, 10, or 25. Add up one of each coin and
print Total: 41 cents.
Show solution
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 coins = [Coin::Penny, Coin::Nickel, Coin::Dime, Coin::Quarter];
let mut total = 0;
for coin in &coins {
total += value_in_cents(coin);
}
println!("Total: {total} cents");
}