---
title: Enums
source: 'https://academia.sh/en/courses/rust-fundamentals/enums'
course: 'Rust Fundamentals'
language: en
updated: '2026-09-15T04:58:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that constructing a variant is an expression and an enum declaration is an item'
    - 'Explains that variants carrying data and variants carrying none are counted under the same type'
    - 'Distinguishes, with an example, the difference between the number of variants and the number of states a value can carry'
---

# Enums

A Shape value can be constructed in three separate forms — carrying no data, carrying one number, carrying two numbers — and all three are counted under the same type; constructing a variant is an expression, the enum declaration itself is an item. The limiting measurement shows that variant count must not be confused with state count.

The Structs lesson measured that a `Point` always carries both `x` and `y`: a struct is a
value bound to a fixed set of fields, with a single shape. But the data a program has to
model often takes more than one possible shape in an "either this or that" way — a shape
that has not been drawn at all, a shape that is a circle, or a shape that is a rectangle.
A struct alone cannot meet this; the enum, the subject of this lesson, fills exactly that
gap.

An enum sets up a value being able to carry one of several separate **variants** under the
same type. The same question from the previous lesson applies here too: does constructing a
variant produce a value when it runs, or does the `enum` declaration itself produce
something? The answer follows the same order as structs, but this time the difference
between **variant count** and the number of states a value can carry is measured as well.

What separates an enum from a set of constants that do not serve the same function is that
each variant can optionally carry its own data. Three separately defined constants — say
`EMPTY`, `CIRCLE`, `RECTANGLE` — are three independent names; there is no guarantee the
compiler gives for the question "which one is selected." An `enum`, however, builds a
single type, and every value of that type knows within itself which variant it currently
carries. The first thing this lesson measures is how this single-typedness looks on the
expression/statement/item axis.

## The Declaration Is Still an Item, Constructing a Variant Is an Expression

The line `enum Shape { Empty, Circle(f64), Rectangle(f64, f64) }`, just like the `struct`
declaration in the previous lesson, produces nothing when it runs. This line introduces the
compiler to a type with three separate variants; it is itself an item. By contrast, the
syntax `Shape::Circle(2.0)` produces a value when it runs — not an integer, but a `Shape`
value carrying the `Circle` variant with `2.0` inside it. This is an expression; its result
can be bound to a variable, appended to a list, passed to a function.

**DM5.** — The claim that "an `enum` declaration by itself produces nothing" is verified
here, just as with the `struct` example in the previous lesson, not by a separate run but
by the structure of the program below: the line `enum Shape { ... }` sits before `main`,
and no print call directly triggers that line; every visible output comes from the lines
where variants are **constructed** and **used**.

`Empty`, `Circle`, and `Rectangle` are three separate variant names, and all three are
listed, separated by commas, inside a single `enum Shape { }` declaration. The difference
between `Circle(f64)` and `Rectangle(f64, f64)` is the number of types inside the
parentheses: one carries a single `f64`, the other carries two `f64`s; `Empty` does not
even have parentheses, because it carries no data at all. These three forms — no
parentheses, one field, several fields — cover the full range of variant forms an `enum`
declaration allows.

The run below constructs four values of type `Shape` and prints which variant each one
belongs to and what its area is.

```rust
// shape_enum.rs — an enum declaration is an item, constructing a variant is an expression
use std::collections::BTreeSet;

enum Shape {
    Empty,
    Circle(f64),
    Rectangle(f64, f64),
}

fn variant_name(s: &Shape) -> &'static str {
    match s {
        Shape::Empty => "Empty",
        Shape::Circle(_) => "Circle",
        Shape::Rectangle(_, _) => "Rectangle",
    }
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Empty => 0.0,
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(width, height) => width * height,
    }
}

fn main() {
    let shapes = vec![
        Shape::Empty,
        Shape::Circle(2.0),
        Shape::Circle(5.0),
        Shape::Rectangle(3.0, 4.0),
    ];

    println!("{:<20}: {}", "carried value count", shapes.len());
    for (i, s) in shapes.iter().enumerate() {
        println!(
            "{:<20}: {:<11} area={:.2}",
            format!("shapes[{}] variant", i),
            variant_name(s),
            area(s)
        );
    }

    let distinct_variants: BTreeSet<&str> = shapes.iter().map(variant_name).collect();
    println!("{:<20}: {}", "distinct variants", distinct_variants.len());
}
```

```
carried value count : 4
shapes[0] variant   : Empty       area=0.00
shapes[1] variant   : Circle      area=12.57
shapes[2] variant   : Circle      area=78.54
shapes[3] variant   : Rectangle   area=12.00
distinct variants   : 3
```

`shapes` was built with four separate calls, and each call was an expression: `Shape::Empty`
is a `Shape` value even though it carries no data at all; `Shape::Circle(2.0)` reaches the
same result carrying one decimal number, `Shape::Rectangle(3.0, 4.0)` reaches it carrying
two decimal numbers. All three could be `push`ed into a `Vec<Shape>` — for the compiler they
are all the same type, `Shape`.

The `variant_name` and `area` functions are used here as a tool: both use `match` to read
which variant a value carries and produce a result specific to that variant. `match`'s own
rules and the exhaustiveness of its arms are the subject of the next lesson; here it is only
visible that reaching the data from a data-carrying variant (`Circle`, `Rectangle`) requires
a route — the data-free `Empty` variant has no field to read, so the `area` function gives
it `0.0` directly.

The numbers `area` gives make this concrete: `Circle(2.0)`'s area is $\pi \times 2^2 \approx
12.57$, `Circle(5.0)`'s area is $\pi \times 5^2 \approx 78.54$. Both use the same formula —
the square of the radius times pi — but because the carried number differs, so does the
result. `Rectangle(3.0, 4.0)`'s area is `12.00`; this variant carries not one number but two,
and the `area` function reads and multiplies both. The `Empty` variant carries no number at
all, so there is no area to compute for it — `area` covers that case in its own arm with
`0.0`.

To count the variant names, an ordered set (`BTreeSet`) was used here rather than a hash
set, even though only `.len()` is read and a set's element count does not depend on
traversal order — for this single line, a hash set would give the same result. The choice
here comes not from necessity but from a habit that carries through the course: whenever a
structure whose output does not change from run to run is needed, the ordered structure is
the default choice. When this choice is required and when it is merely for consistency is
the subject of the Ordered and Set Structures lesson.

## Variants Carrying Data and Variants Carrying None Are Counted Under the Same Type

One of `Shape`'s three variants (`Empty`) carries no data at all; two (`Circle`,
`Rectangle`) do. In the run above, `shapes.len()` counted all four; the `Shape` type holds
all of them with equal weight. This is the fundamental property that separates Rust's enum
from a list ("which option?"): it can carry not just a name but optionally a **payload**
as well. A data-free variant is not a special case of a data-carrying variant whose payload
happens to be zero — both are equally valued members of the same `enum` declaration.

This equality is also why a collection like `Vec<Shape>` is possible in the first place. A
collection carries a single type; if `Empty`, `Circle`, and `Rectangle` were three separate
types, holding all three in the same list would not be possible. `enum` solves this by
combining them under a single type: the actual variant a `Shape` variable carries is stored
in memory at runtime, but at compile time `Shape` is seen as a single type.

**DM6.** — The claim that "variants carrying data and variants carrying none are counted
under the same type" is measured here by all four being able to go into a single
`Vec<Shape>`: if the variants were counted as different types, the compiler would reject
the fourth item in `shapes` (`Rectangle`) as a different type from the first (`Empty`). The
compile succeeding cleanly is, together with the `carried value count : 4` line counting all
four, direct proof of this equality.

Variant names are not written alone but together with the type name: `Shape::Circle(2.0)`,
not `Circle(2.0)`. What requires this prefix is that the same name can be used as a variant
in more than one `enum` — if another type also had a variant named `Circle` defined, which
type's `Circle` was being referred to would be ambiguous without the `Shape::` prefix. This
is a different point from constructing a struct value (`Point { x: 3, y: 4 }`): in
struct-construction syntax the type name is already at the front and no repeated prefix is
needed, but in variant-construction syntax the type name and the variant name are two
separate names, so a `::` is placed between them.

This single-typedness is made possible by every `Shape` value knowing within itself which
variant it carries: the `variant_name` function reads exactly this piece of information;
the `area` function both reads which variant it is and, based on that, accesses different
fields. There is no such distinction in a struct, because all of a struct's fields are
always present at the same time — a `Point` value never falls into a "only has `x` right
now" state. `enum` reverses this: a value has the fields of only **one** variant at any
given time, the fields of the other variants do not exist at all for that value.

This does not mean the value `Shape::Rectangle(3.0, 4.0)` also carries a `radius` field at
the same time — that value carries only `width` and `height`, the `Circle` variant's field
is entirely separate from it. Each arm of the `match` expression in the `area` function can
only reach the fields of its own variant; inside the `Shape::Circle(radius) => ...` arm,
reaching a `width` name is not possible, because that arm only runs when the `Circle`
variant matches, and there is no `width` at that point.

This does not touch on the question of how many bytes are allocated in memory — the lesson
does not print such a number — it only measures which fields are **accessible** in which
state. Accessibility is a source-level rule expressed by `match`'s arm structure: only the
fields of the variant a given arm matches enter that arm.

## Limiting Measurement: Variant Count Is Not State Count

`Shape` carries three variants: `Empty`, `Circle`, `Rectangle`. But "three variants" and
"three possible values" are not the same thing. `Empty` has exactly one state it can
carry — itself. `Circle`, however, carries an `f64`, and the number of values an `f64` can
take is uncountably large. The same variant, with different radii, can produce infinitely
many different **states**.

```rust
// same_variant_different_state.rs — variant count is not state count
enum Shape {
    Circle(f64),
}

fn main() {
    let small = Shape::Circle(2.0);
    let large = Shape::Circle(5.0);

    let Shape::Circle(r1) = &small;
    let Shape::Circle(r2) = &large;

    println!("{:<24}: {}", "small circle radius", r1);
    println!("{:<24}: {}", "large circle radius", r2);
    println!("{:<24}: {}", "same variant", true);
    println!("{:<24}: {}", "same state", r1 == r2);
}
```

```
small circle radius     : 2
large circle radius     : 5
same variant            : true
same state              : false
```

`small` and `large` both carry the `Circle` variant — the answer to the "same variant"
question is `true`. But they do not carry the same state: one has radius `2.0`, the other
`5.0` — the answer to the "same state" question is `false`. The "three separate variants"
count measured in the previous run does not say how many different **states** a
data-carrying variant like `Circle` can appear in.

**DM7.** — This limiting measurement is done here by constructing two separate
`Shape::Circle` values in a single code block and printing side by side that both come from
the same variant (`true`) but carry a different radius (the result of `r1 == r2` is
`false`). "Variant count" can be found by reading the `enum` declaration from the source
(three lines, three variants), while "state count" can only be found by looking at which
values are produced while the program runs; the two answer different questions.

This distinction disappears for a variant that carries no data. `Empty` has exactly one
state, because it carries no field at all — variant count and state count coincide there.
The gap opens only for data-carrying variants, and from the moment it opens, state count
becomes dependent on the type of the data carried: a variant carrying a `bool` has two
states, a variant carrying an `f64` has, in practice, an unbounded number of states.

This holds even for a single-field variant; as the field count grows, the state count grows
multiplicatively. `Rectangle(f64, f64)` carries two separate `f64`s — width and height can
vary independently of each other, so this one variant's state space is not
one-dimensional but a two-dimensional infinity. The run above only showed
`Rectangle(3.0, 4.0)`, but the declaration `Rectangle(f64, f64)` written in the source
allows for far more than that: `Rectangle(1.0, 1.0)`, `Rectangle(0.5, 100.0)`, and
countless other pairs all belong to the same variant, but none of them share the same
state.

This is why the answer to "how many states" is given not by looking at the `enum`
declaration, but by looking at the types of its fields. A data-free variant adds exactly `1`
to the state count; a variant carrying a type with a fixed number of separate values (for
example, another `enum` that itself has a limited number of variants) contributes as many
states as that type has; a variant carrying an unbounded type like `f64` or `String` makes
the state count unbounded. The three-line `enum Shape { ... }` declaration in the source
shows none of this anywhere — all three are written in the same form, as a variant name.

This distinction has a practical consequence: the correctness of code that handles a
`Shape` value is measured by the question "were all three variants handled?" — not by the
question "were all infinite states tested?" If the `area` function gives the correct result
for `Circle(2.0)`, it gives the correct result for `Circle(5.0)` too, because both are
processed in the same arm; there is no need to test every single radius separately. The
compiler's exhaustiveness check also operates exactly at the variant level, not the state
level — this boundary already decides in advance what the "exhaustiveness" concept measured
in the next lesson does and does not guarantee.

This lesson measured how many separate forms a `Shape` value can take and how those forms
are constructed in the source. But constructing a value is only one half; **reading** which
variant the constructed value carries is the other half, and this lesson used that reading
only in the background, inside two helper functions (`variant_name`, `area`). `match`
itself — which pattern forms exist, when one arm comes before another, why the compiler
requires every variant to be handled — was never opened.

## Summary

- An `enum` declaration is an item: it produces no value when it runs, it only defines the
  variants.
- Constructing a variant (like `Shape::Circle(2.0)`) is an expression and produces a value.
- Variants carrying data and variants carrying none are counted with equal weight under the
  same `enum` type; this is what lets a single collection (like `Vec<Shape>`) hold all of
  them together.
- Variant count is a fixed number read from the `enum` declaration in the source.
- State count is different from this: a data-carrying variant can produce one, a few, or an
  uncountably large number of states, depending on the type of the data it carries.

## Next Step

This lesson measured how many separate variants a `Shape` value can carry and how
constructing a variant is an expression; the `variant_name` and `area` functions used
`match` to tell these variants apart but never entered `match`'s own rules. The next lesson
fills exactly that gap: how is which variant a `Shape` value carries read from the source,
how many separate pattern forms can do this, and why does the compiler require every
variant to be handled?
