🎓 A1Learner
🔍

Rust Enums

An enum is a type whose value is exactly one of a few named kinds, called variants. Each variant can carry its own data.

Name the kinds with enum

Each value of the enum is exactly one of the named kinds, and variants live under the enum name:

#[derive(Debug)]
enum Light { Red, Yellow, Green }
let light = Light::Green;
println!("light = {:?}", light);

Variants can carry data

A variant may carry data: no data, a tuple of values, or named fields. A variant with data works like a small constructor — it builds a value and stores the data inside it:

#[derive(Debug)]
enum IpAddr { V4(u8, u8, u8, u8), V6(String) }
let home = IpAddr::V4(127, 0, 0, 1);
println!("home = {:?}", home);

match handles every variant

Use match to handle every variant — the compiler checks that none is forgotten:

enum Direction { Left, Right }
let turn = Direction::Left;
let word = match turn { Direction::Left => "turn left", Direction::Right => "turn right" };
println!("{}", word);

Option: a value, or none

Option<T> is Rust's built-in enum for "maybe a value": Some(value) or None. Some and None are so common that they are written bare, with no Option:: in front:

let found: Option<i32> = Some(42);
let missing: Option<i32> = None;
// let total = found + 5; // error: an Option is not a number
println!("found = {:?} missing = {:?}", found, missing);

Take the value out with unwrap_or

unwrap_or takes the value out of an Option, with a fallback when it is None:

let missing: Option<i32> = None;
let present: Option<i32> = Some(7);
println!("{} {}", missing.unwrap_or(0), present.unwrap_or(0));

Option<&T> takes no extra room

Rust packs Option<&T> tightly: it takes exactly as much room as the &T itself, measured here with std::mem::size_of. The None case hides in the empty address 0, which a real reference can never hold, so no extra tag is stored:

println!("Option<&i32> = {} bytes", std::mem::size_of::<Option<&i32>>());
println!("&i32 = {} bytes", std::mem::size_of::<&i32>());

Example

fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
    println!("{:?}", divide(10.0, 2.0));
    println!("{:?}", divide(10.0, 0.0));
    println!("{}", divide(10.0, 0.0).unwrap_or(f64::INFINITY));
}

Key points

  • An Option is a different type from the value inside: the value must be taken out first.
  • None alone gives Rust no clue about the type, so the type must be named.
  • A struct, in contrast, holds all of its fields at once.

In this example, divide returns an Option<f64>. If the second number is 0.0, division is impossible, so it returns None. Otherwise it returns Some of the answer. The first call prints Some(5.0). The second prints None — no crash, no fake answer. The third line shows one way to handle None. unwrap_or steps in with a fallback value, here infinity. It prints inf. The caller always chooses what "no value" means.

Note

Rust has no null.

"No value" is written as None, and the type forces every caller to deal with it.

That is why Rust programs do not crash with null-pointer errors.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

How does Rust represent "no value"?