Enums with data

In Rust an enum is a type with a fixed list of variants, and each variant may carry its own data. It is the Rust answer to a tagged union, and it is used everywhere in the standard library: Option and Result are enums, and so is the error type of a parser. The compiler knows every variant, so a match can be checked for completeness.

Coming from C++? Enums and variants puts a std::variant next to an enum with data, and then reads the wrong alternative in both languages: one throws at run time, the other does not compile.

The lesson starts with an enum of names, adds data to the variants, adds methods, and builds a recursive tree with Box. The last section shows what a wrong payload type does.

An enum of names

The variants of a plain enum are values of the type, and a match maps each one to a result. The compiler checks that every variant is covered, so adding a fifth direction later would break every match that ignores it.

The program turns four directions into steps and prints the pair for each. The match is the only place that knows the meaning of a direction.

enum Direction { North, South, East, West, } fn step(direction: Direction) -> (i32, i32) { match direction { Direction::North => (0, 1), Direction::South => (0, -1), Direction::East => (1, 0), Direction::West => (-1, 0), } } fn main() { let directions = [Direction::North, Direction::South, Direction::East, Direction::West]; for direction in directions { let (x, y) = step(direction); println!("{x} {y}"); } }

rustc --edition 2024 simple.rs
./simple

0 1
0 -1
1 0
-1 0

A plain enum is small enough to be a byte, and comparing two variants is a comparison of that byte. The lesson on patterns covers the match forms that go with it.

Variants that carry data

A variant may hold a value, several values or named fields, and each variant decides its own shape. The match then destructures the payload: Shape::Circle(r) binds the radius, and Shape::Rectangle { width, height } binds the two fields.

The program keeps two different shapes in one array and matches over a reference. The output prints the payload of each variant, which is the data the variant was built with.

enum Shape { Circle(f64), Rectangle { width: f64, height: f64 }, } fn main() { let shapes = [Shape::Circle(1.0), Shape::Rectangle { width: 2.0, height: 3.0 }]; for shape in &shapes { match shape { Shape::Circle(r) => println!("circle {r}"), Shape::Rectangle { width, height } => println!("rectangle {width} {height}"), } } }

rustc --edition 2024 data.rs
./data

circle 1
rectangle 2 3

The two variants may have different sizes, and the enum takes the size of the largest plus a tag. The lesson on memory layout measures that, and the one on trait objects shows the alternative when the set of variants is open.

Methods on an enum

An impl block works on an enum exactly as it does on a struct. The method matches on self, which is a reference to the whole value, and each arm computes with the payload of its variant.

The program asks a circle and a square for their area. The two calls run the same method, and the match inside picks the formula.

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 main() { println!("{:.2} {:.2}", Shape::Circle(1.0).area(), Shape::Square(3.0).area()); }

rustc --edition 2024 methods.rs
./methods

3.14 9.00

The method is the replacement for a virtual function when the variants are known in advance: the dispatch happens at compile time, and there is no vtable. The lesson on the visitor pattern compares the two designs.

A recursive enum

A variant cannot contain the enum directly, because the size would be infinite. It contains a Box<Expr> instead, a pointer to a value on the heap, and the enum becomes a tree.

The program builds two plus three plus four and evaluates it. The recursive call in eval walks the tree, and the match has one arm per node kind.

enum Expr { Number(i64), Add(Box<Expr>, Box<Expr>), } fn eval(expr: &Expr) -> i64 { match expr { Expr::Number(n) => *n, Expr::Add(left, right) => eval(left) + eval(right), } } fn main() { let sum = Expr::Add( Box::new(Expr::Number(2)), Box::new(Expr::Add(Box::new(Expr::Number(3)), Box::new(Expr::Number(4)))), ); println!("{}", eval(&sum)); }

rustc --edition 2024 expr_tree.rs
./expr_tree

9

Box is the only heap allocation in the program, and it exists to break the size recursion. The lesson on Box builds the same type from scratch.

A variant of the wrong type

A variant declares the type of its payload, and building it with another type is E0308. The message names the variant, the expected type and the value, so the mistake is located in one line.

The program builds a circle with a string where the radius belongs. The error points at the argument and at the variant definition, and the debug placeholder in the print is never reached.

enum Shape { Circle(f64), } fn main() { let shape = Shape::Circle("one"); println!("{shape:?}"); }

rustc --edition 2024 variant_type_err.rs

error[E0308]: mismatched types
--> variant_type_err.rs:6:31
|
6 | let shape = Shape::Circle("one");
| ------------- ^^^^^ expected `f64`, found `&str`
| |
| arguments to this enum variant are incorrect
|
note: tuple variant defined here
--> variant_type_err.rs:2:5
|
2 | Circle(f64),
| ^^^^^^

error[E0277]: `Shape` doesn't implement `Debug`
--> variant_type_err.rs:7:15
|
7 | println!("{shape:?}");
| ^^^^^^^^^ `Shape` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
= help: the trait `Debug` is not implemented for `Shape`
= note: add `#[derive(Debug)]` to `Shape` or manually `impl Debug for Shape`
help: consider annotating `Shape` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | enum Shape {
|

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.

The payload type is part of the variant, not a free-form value: a circle always has a number, and a rectangle always has its two fields. That is why an enum can replace a class hierarchy without losing type safety.

Exercises

  1. Add a third direction to the first enum and let the compiler list the matches that need a new arm.
  2. Add a Triangle variant with a base and a height, and extend the area method.
  3. Add a Multiply node to the expression tree and evaluate an expression that uses it.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is an enum variant allowed to carry?

    Answer

    Its own set of values or named fields

    Each variant declares its own payload, which is what makes the type a tagged union.

  2. What does a match over an enum without a wildcard guarantee?

    Answer

    That every variant is handled, or the build fails

    The compiler checks exhaustiveness and names any variant that is missing.

  3. Why does a recursive enum need a Box?

    Answer

    Because a variant cannot contain the enum at its full size

    The pointer has a fixed size, which breaks the infinite size recursion.

  4. How much space does an enum take?

    Answer

    Enough for its largest variant plus a tag

    The variants share one storage area, and a tag records which one is live.

  5. What does this program print? enum Direction { North, South, East, West, } fn main() { let direction = Direction::East; match direction { Direction::North => println!("north"), Direction::South => println!("south"), Direction::East => println!("east"), Direction::West => println!("west"), } }

    Answer

    east

    The value is the East variant, and its arm prints the lower-case word.

  6. What does this program print? enum Shape { Circle(f64), Rectangle { width: f64, height: f64 }, } fn main() { let shapes = [Shape::Circle(1.0), Shape::Rectangle { width: 2.0, height: 3.0 }]; for shape in &shapes { match shape { Shape::Circle(r) => println!("circle {r}"), Shape::Rectangle { width, height } => println!("rectangle {width} {height}"), } } }

    Answer

    circle 1 / rectangle 2 3

    The array holds both variants, and each arm prints the payload of its own variant.

  7. What does this program print? 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 main() { println!("{:.2}", Shape::Square(3.0).area()); }

    Answer

    9.00

    The square arm multiplies three by three, and the format prints two decimals.

  8. What does this program print? enum Expr { Number(i64), Add(Box<Expr>, Box<Expr>), } fn eval(expr: &Expr) -> i64 { match expr { Expr::Number(n) => *n, Expr::Add(left, right) => eval(left) + eval(right), } } fn main() { let sum = Expr::Add( Box::new(Expr::Number(2)), Box::new(Expr::Add(Box::new(Expr::Number(3)), Box::new(Expr::Number(4)))), ); println!("{}", eval(&sum)); }

    Answer

    9

    The tree adds two to the sum of three and four, which is nine.

  9. What is the first line of the compiler output for this program? enum Shape { Circle(f64), } fn main() { let shape = Shape::Circle("one"); println!("{shape:?}"); }

    Answer

    error[E0308]: mismatched types

    The variant expects a number and gets a string, and the note points at the variant definition.

  10. What does this program print? enum Light { Red, Green, Amber, } fn next(light: Light) -> Light { match light { Light::Red => Light::Green, Light::Green => Light::Amber, Light::Amber => Light::Red, } } fn main() { let light = next(next(Light::Red)); match light { Light::Red => println!("red"), Light::Green => println!("green"), Light::Amber => println!("amber"), } }

    Answer

    amber

    Red goes to green and green goes to amber, and the second match prints that variant.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev