Rust - Enums
Overview
Estimated time: 55–75 minutes
Learn how to define and use enums in Rust, including pattern matching with match expressions. Enums are powerful tools for expressing data that can be one of several variants.
Learning Objectives
- Define enums with different variant types
- Use pattern matching with match expressions
- Understand Option and Result enums
- Work with enums that contain data
Prerequisites
Basic Enums
Enums allow you to define types that can be one of several variants:
enum Direction {
North,
South,
East,
West,
}
fn main() {
let direction = Direction::North;
match direction {
Direction::North => println!("Going north!"),
Direction::South => println!("Going south!"),
Direction::East => println!("Going east!"),
Direction::West => println!("Going west!"),
}
}
Expected output:
Going north!
Enums with Data
Enum variants can hold different types and amounts of data:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("Quit message received");
}
Message::Move { x, y } => {
println!("Move to coordinates ({}, {})", x, y);
}
Message::Write(text) => {
println!("Text message: {}", text);
}
Message::ChangeColor(r, g, b) => {
println!("Change color to RGB({}, {}, {})", r, g, b);
}
}
}
fn main() {
let messages = vec![
Message::Quit,
Message::Move { x: 10, y: 20 },
Message::Write(String::from("Hello, world!")),
Message::ChangeColor(255, 0, 128),
];
for message in messages {
process_message(message);
}
}
Expected output:
Quit message received
Move to coordinates (10, 20)
Text message: Hello, world!
Change color to RGB(255, 0, 128)
The Option Enum
Option is used to represent values that might be absent, replacing null pointers:
fn find_word(text: &str, word: &str) -> Option {
text.find(word)
}
fn divide(x: f64, y: f64) -> Option {
if y != 0.0 {
Some(x / y)
} else {
None
}
}
fn main() {
let text = "Hello, world!";
match find_word(text, "world") {
Some(index) => println!("Found 'world' at index {}", index),
None => println!("'world' not found"),
}
match find_word(text, "rust") {
Some(index) => println!("Found 'rust' at index {}", index),
None => println!("'rust' not found"),
}
// Using Option with arithmetic
let result1 = divide(10.0, 2.0);
let result2 = divide(10.0, 0.0);
match result1 {
Some(value) => println!("10.0 / 2.0 = {}", value),
None => println!("Division by zero!"),
}
match result2 {
Some(value) => println!("10.0 / 0.0 = {}", value),
None => println!("Division by zero!"),
}
}
Expected output:
Found 'world' at index 7
'rust' not found
10.0 / 2.0 = 5
Division by zero!
Working with Option
Common patterns for working with Option values:
fn main() {
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option = None;
// Using if let for simple cases
if let Some(value) = some_number {
println!("Got a value: {}", value);
}
// Using unwrap_or for default values
let x = absent_number.unwrap_or(0);
println!("Value or default: {}", x);
// Using map to transform values
let doubled = some_number.map(|n| n * 2);
println!("Doubled: {:?}", doubled);
// Chaining operations
let result = some_number
.map(|n| n * 3)
.map(|n| n + 1)
.unwrap_or(0);
println!("Chained operations: {}", result);
}
Expected output:
Got a value: 5
Value or default: 0
Doubled: Some(10)
Chained operations: 16
Pattern Matching
Match expressions provide powerful pattern matching capabilities:
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String), // Quarter with state name
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {}!", state);
25
}
}
}
fn main() {
let coins = vec![
Coin::Penny,
Coin::Nickel,
Coin::Dime,
Coin::Quarter(String::from("Alaska")),
];
let mut total = 0;
for coin in coins {
total += value_in_cents(coin);
}
println!("Total value: {} cents", total);
}
Expected output:
Lucky penny!
State quarter from Alaska!
Total value: 41 cents
Advanced Pattern Matching
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
Kelvin(f64),
}
fn describe_temperature(temp: Temperature) {
match temp {
Temperature::Celsius(c) if c < 0.0 => {
println!("{:.1}°C - Freezing!", c);
}
Temperature::Celsius(c) if c > 30.0 => {
println!("{:.1}°C - Hot!", c);
}
Temperature::Celsius(c) => {
println!("{:.1}°C - Comfortable", c);
}
Temperature::Fahrenheit(f) => {
let celsius = (f - 32.0) * 5.0 / 9.0;
println!("{:.1}°F ({:.1}°C)", f, celsius);
}
Temperature::Kelvin(k) => {
let celsius = k - 273.15;
println!("{:.1}K ({:.1}°C)", k, celsius);
}
}
}
fn main() {
let temperatures = vec![
Temperature::Celsius(-5.0),
Temperature::Celsius(25.0),
Temperature::Celsius(35.0),
Temperature::Fahrenheit(100.0),
Temperature::Kelvin(300.0),
];
for temp in temperatures {
describe_temperature(temp);
}
}
Expected output:
-5.0°C - Freezing!
25.0°C - Comfortable
35.0°C - Hot!
100.0°F (37.8°C)
300.0K (26.9°C)
The Result Enum
Result is used for operations that might fail:
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result {
s.parse()
}
fn divide_strings(dividend: &str, divisor: &str) -> Result {
let num1: f64 = dividend.parse()
.map_err(|_| format!("Invalid dividend: '{}'", dividend))?;
let num2: f64 = divisor.parse()
.map_err(|_| format!("Invalid divisor: '{}'", divisor))?;
if num2 == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(num1 / num2)
}
}
fn main() {
// Working with parse results
let inputs = vec!["42", "abc", "0", "-5"];
for input in inputs {
match parse_number(input) {
Ok(num) => println!("'{}' parsed as: {}", input, num),
Err(e) => println!("Failed to parse '{}': {}", input, e),
}
}
// Working with complex Results
let operations = vec![
("10", "2"),
("15", "3"),
("8", "0"),
("abc", "5"),
];
for (dividend, divisor) in operations {
match divide_strings(dividend, divisor) {
Ok(result) => println!("{} / {} = {:.2}", dividend, divisor, result),
Err(error) => println!("Error: {}", error),
}
}
}
Expected output:
'42' parsed as: 42
Failed to parse 'abc': invalid digit found in string
'0' parsed as: 0
'-5' parsed as: -5
10 / 2 = 5.00
15 / 3 = 5.00
Error: Division by zero
Error: Invalid dividend: 'abc'
Methods on Enums
You can implement methods on enums just like structs:
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(radius) => {
std::f64::consts::PI * radius * radius
}
Shape::Rectangle(width, height) => {
width * height
}
Shape::Triangle(a, b, c) => {
// Using Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn perimeter(&self) -> f64 {
match self {
Shape::Circle(radius) => {
2.0 * std::f64::consts::PI * radius
}
Shape::Rectangle(width, height) => {
2.0 * (width + height)
}
Shape::Triangle(a, b, c) => {
a + b + c
}
}
}
fn describe(&self) -> String {
match self {
Shape::Circle(radius) => {
format!("Circle with radius {:.1}", radius)
}
Shape::Rectangle(width, height) => {
format!("Rectangle {}×{}", width, height)
}
Shape::Triangle(a, b, c) => {
format!("Triangle with sides {:.1}, {:.1}, {:.1}", a, b, c)
}
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(4.0, 6.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in shapes {
println!("{}", shape.describe());
println!(" Area: {:.2}", shape.area());
println!(" Perimeter: {:.2}", shape.perimeter());
println!();
}
}
Expected output:
Circle with radius 5.0
Area: 78.54
Perimeter: 31.42
Rectangle 4×6
Area: 24.00
Perimeter: 20.00
Triangle with sides 3.0, 4.0, 5.0
Area: 6.00
Perimeter: 12.00
Common Pitfalls
1. Non-exhaustive Match
Match expressions must cover all possible variants:
enum Color {
Red,
Green,
Blue,
}
fn main() {
let color = Color::Red;
// Wrong: missing Blue variant
// match color {
// Color::Red => println!("Red"),
// Color::Green => println!("Green"),
// // Missing Color::Blue - compiler error!
// }
// Correct: handle all variants
match color {
Color::Red => println!("Red"),
Color::Green => println!("Green"),
Color::Blue => println!("Blue"),
}
// Or use a catch-all
match color {
Color::Red => println!("It's red!"),
_ => println!("Some other color"),
}
}
2. Using unwrap() on Option/Result
fn main() {
let maybe_number: Option = None;
// Dangerous: will panic if None
// let number = maybe_number.unwrap();
// Better: handle the None case
match maybe_number {
Some(num) => println!("Got number: {}", num),
None => println!("No number available"),
}
// Or use unwrap_or for a default
let number = maybe_number.unwrap_or(0);
println!("Number or default: {}", number);
}
Checks for Understanding
Question 1: What's the difference between Option<T>
and Result<T, E>
?
Answer: Option<T>
represents a value that might be absent (Some(T) or None). Result<T, E>
represents an operation that might fail, returning either success (Ok(T)) or an error (Err(E)).
Question 2: What happens if you don't handle all enum variants in a match?
Answer: The Rust compiler will produce a compile-time error. Match expressions must be exhaustive - they must handle all possible variants or use a catch-all pattern like _
.
Question 3: How would you define an enum for different HTTP status codes?
Answer:
enum HttpStatus {
Ok,
NotFound,
InternalServerError,
BadRequest(String), // With error message
}