Enums
Data · Lesson 22
A Rust enum is a type with a fixed list of variants, and each variant can carry its own data. The compiler knows the full list, so it can check that you handled every variant.
Coming from GoGo has no sum types. The closest is an interface plus a type switch (or iota constants, when there is no data). An interface is open: anyone can add a type, and Go does not check that a type switch covers them all. A Rust enum is a closed list of variants, and match won't compile until every variant is handled.
Coming from KotlinA Rust enum is your sealed class, and each variant is like a data subclass, with match checking every case the way when does. One difference: a variant is not its own type, so you can't have a parameter of type Shape::Circle.
Coming from PythonThis is your union of dataclasses (Circle | Rect) plus match, in one declaration. The compiler checks that every variant is handled, and Python's Enum only covers variants that all hold the same kind of data, not different data per variant.
Coming from TypeScriptA Rust enum is your discriminated union, but the compiler keeps the tag for you, so there is no kind field to write. match checks every variant without the never trick, and a _ arm turns that check off the way default: does.
Coming from JavaA Rust enum with data is a sealed interface plus one record per variant, and match is the exhaustive pattern switch. Rust needs no separate types; the variants live inside the enum itself, as plain values rather than objects, and a variant is not a type of its own.
Coming from C#A data-carrying enum is an abstract record with one derived record per case, and match is the positional pattern switch. On .NET 10 (C# 14) the compiler does not know that list is complete, so a missing case is a CS8509 warning and a run-time exception. In Rust it is a compile error. C# 15 adds union and closed types for this.
Coming from CA Rust enum is a tagged union (an enum tag plus a union) that the compiler manages for you. You can't read the wrong field for the current tag, and a match that forgets a variant doesn't compile.
Coming from C++A Rust enum is a std::variant, except each alternative is a named variant, not a separate type, and match replaces std::visit with the overloaded helper. A missing case gets a short, clear error instead of a long template error.
The quick comparison
enum Shape { Circle(f64), // radius Rect(f64, f64), // width, height}
fn area(s: &Shape) -> f64 { match s { Shape::Circle(r) => 3.14 * r * r, Shape::Rect(w, h) => w * h, }}
fn main() { let shapes = [Shape::Circle(1.0), Shape::Rect(2.0, 3.0)]; for s in &shapes { println!("{}", area(s)); }}3.14 6
package main
import "fmt"
type Shape interface{}type Circle struct{ R float64 }type Rect struct{ W, H float64 }
func area(s Shape) float64 { switch v := s.(type) { case Circle: return 3.14 * v.R * v.R case Rect: return v.W * v.H } return 0}
func main() { shapes := []Shape{Circle{1.0}, Rect{2.0, 3.0}} for _, s := range shapes { fmt.Println(area(s)) }}3.14 6
sealed class Shape { data class Circle(val r: Double) : Shape() data class Rect(val w: Double, val h: Double) : Shape()}
fun area(s: Shape): Double = when (s) { is Shape.Circle -> 3.14 * s.r * s.r is Shape.Rect -> s.w * s.h}
fun main() { val shapes = listOf(Shape.Circle(1.0), Shape.Rect(2.0, 3.0)) for (s in shapes) { println(area(s)) }}3.14 6.0
from dataclasses import dataclass
@dataclassclass Circle: r: float
@dataclassclass Rect: w: float h: float
Shape = Circle | Rect
def area(s: Shape) -> float: match s: case Circle(r): return 3.14 * r * r case Rect(w, h): return w * h
for s in [Circle(1.0), Rect(2.0, 3.0)]: print(area(s))3.14 6.0
// a discriminated union: `kind` says which variant it istype Shape = | { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number };
function area(s: Shape): number { switch (s.kind) { case "circle": return 3.14 * s.r * s.r; case "rect": return s.w * s.h; default: { const unreachable: never = s; // fails to compile if a case is missing return unreachable; } }}
const shapes: Shape[] = [{ kind: "circle", r: 1.0 }, { kind: "rect", w: 2.0, h: 3.0 }];for (const s of shapes) { console.log(area(s));}3.14 6
import java.util.List;
public class Main { sealed interface Shape permits Circle, Rect {} record Circle(double r) implements Shape {} record Rect(double w, double h) implements Shape {}
static double area(Shape s) { return switch (s) { case Circle c -> 3.14 * c.r() * c.r(); case Rect(double w, double h) -> w * h; }; }
public static void main(String[] args) { List<Shape> shapes = List.of(new Circle(1.0), new Rect(2.0, 3.0)); for (var s : shapes) { System.out.println(area(s)); } }}3.14 6.0
Shape[] shapes = [new Circle(1.0), new Rect(2.0, 3.0)];foreach (var s in shapes){ Console.WriteLine(Area(s));}
static double Area(Shape s) => s switch{ Circle(var r) => 3.14 * r * r, Rect(var w, var h) => w * h, _ => throw new ArgumentException("unknown shape"),};
abstract record Shape;record Circle(double Radius) : Shape;record Rect(double Width, double Height) : Shape;3.14 6
#include <stdio.h>
// C enums hold no data, so pair a tag with a union (a "tagged union")enum ShapeKind { CIRCLE, RECT };
struct Shape { enum ShapeKind kind; union { double r; // CIRCLE: radius struct { double w, h; } rect; // RECT: width, height };};
double area(const struct Shape *s) { switch (s->kind) { case CIRCLE: return 3.14 * s->r * s->r; case RECT: return s->rect.w * s->rect.h; } return 0;}
int main(void) { struct Shape shapes[] = { {.kind = CIRCLE, .r = 1.0}, {.kind = RECT, .rect = {2.0, 3.0}}, }; for (int i = 0; i < 2; i++) { printf("%g\n", area(&shapes[i])); } return 0;}3.14 6
#include <iostream>#include <variant>#include <vector>
struct Circle { double r; };struct Rect { double w, h; };using Shape = std::variant<Circle, Rect>;
// a small helper: one lambda per alternativetemplate <class... Fs> struct overloaded : Fs... { using Fs::operator()...; };
double area(const Shape& s) { return std::visit(overloaded{ [](const Circle& c) { return 3.14 * c.r * c.r; }, [](const Rect& r) { return r.w * r.h; }, }, s);}
int main() { std::vector<Shape> shapes{Circle{1.0}, Rect{2.0, 3.0}}; for (const auto& s : shapes) { std::cout << area(s) << "\n"; }}3.14 6
What's different
- In a language without a closed list of cases, you fake it with an interface or base class plus type checks, and nothing makes sure you handled every case. In Rust,
matchchecks that you did. - Rust's
matchpulls the data out of the variant:Shape::Rect(w, h)gives youwandh. - A variant is not a type of its own:
Shape::Circlenames a value, so a function can take aShapebut not aShape::Circle. - An enum with no data works like a C enum:
Level::Mid as i32gives its number. - Option and Result, in the next lessons, are ordinary enums like this one.
Adding a variant and forgetting a case
Looks fine: in Go, Python and C it runs but gives a wrong answer; in C# it crashes at runtime. Rust won't compile it.
Broken in Go. Rust catches it.Kotlin refuses this too. Same idea, different words.Broken in Python. Rust catches it.TypeScript refuses this too. Same idea, different words.Java refuses this too. Same idea, different words.Broken in C#. Rust catches it.Broken in C. Rust catches it.C++ refuses this too. Same idea, different words.enum Shape { Circle(f64), Rect(f64, f64), Triangle(f64, f64), // new!}
fn area(s: &Shape) -> f64 { match s { Shape::Circle(r) => 3.14 * r * r, Shape::Rect(w, h) => w * h, }}
fn main() { println!("{}", area(&Shape::Triangle(2.0, 3.0)));}package main
import "fmt"
type Circle struct{ R float64 }type Rect struct{ W, H float64 }type Triangle struct{ B, H float64 } // new!
func area(s any) float64 { switch v := s.(type) { case Circle: return 3.14 * v.R * v.R case Rect: return v.W * v.H } return 0}
func main() { fmt.Println(area(Triangle{2.0, 3.0}))}0
Kotlin refuses this too: a when over a sealed class must cover every subclass, so adding Triangle breaks the build until you handle it. An else -> branch would silence the check.
sealed class Shape { data class Circle(val r: Double) : Shape() data class Rect(val w: Double, val h: Double) : Shape() data class Triangle(val b: Double, val h: Double) : Shape() // new!}
fun area(s: Shape): Double = when (s) { is Shape.Circle -> 3.14 * s.r * s.r is Shape.Rect -> s.w * s.h}
fun main() { println(area(Shape.Triangle(2.0, 3.0)))}Main.kt:7:30: error: 'when' expression must be exhaustive. Add the 'is Triangle' branch or an 'else' branch.
fun area(s: Shape): Double = when (s) {
^^^^from dataclasses import dataclass
@dataclassclass Circle: r: float
@dataclassclass Rect: w: float h: float
@dataclassclass Triangle: # new! b: float h: float
def area(s: Circle | Rect | Triangle) -> float: match s: case Circle(r): return 3.14 * r * r case Rect(w, h): return w * h
print(area(Triangle(2.0, 3.0)))None
TypeScript refuses this too, because the default branch assigns the leftover value to never and a triangle is not never (TS2322). Without that default line, a function that returns a number still fails under strict (TS2366), but a function that returns nothing compiles, and the missing case goes unnoticed.
type Shape = | { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number } | { kind: "triangle"; b: number; h: number }; // new!
function area(s: Shape): number { switch (s.kind) { case "circle": return 3.14 * s.r * s.r; case "rect": return s.w * s.h; default: { const unreachable: never = s; // fails to compile if a case is missing return unreachable; } }}
console.log(area({ kind: "triangle", b: 2.0, h: 3.0 }));main.ts(13,13): error TS2322: Type '{ kind: "triangle"; b: number; h: number; }' is not assignable to type 'never'.Java refuses this too: a switch expression over a sealed interface must cover every permitted type, so adding Triangle breaks the build until you handle it. The javac message does not name Triangle; Rust's E0004 does.
public class Main { sealed interface Shape permits Circle, Rect, Triangle {} record Circle(double r) implements Shape {} record Rect(double w, double h) implements Shape {} record Triangle(double b, double h) implements Shape {} // new!
static double area(Shape s) { return switch (s) { case Circle c -> 3.14 * c.r() * c.r(); case Rect r -> r.w() * r.h(); }; }
public static void main(String[] args) { System.out.println(area(new Triangle(2.0, 3.0))); }}Main.java:8: error: the switch expression does not cover all possible input values
return switch (s) {
^
1 errorConsole.WriteLine(Area(new Triangle(2.0, 3.0)));
static double Area(Shape s) => s switch{ Circle(var r) => 3.14 * r * r, Rect(var w, var h) => w * h,};
abstract record Shape;record Circle(double Radius) : Shape;record Rect(double Width, double Height) : Shape;record Triangle(double Base, double Height) : Shape; // new!Program.cs(3,34): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '_' is not covered.
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was Triangle { Base = 2, Height = 3 }.
at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
at Program.<<Main>$>g__Area|0_0(Shape s) in Program.cs:line 3
at Program.<Main>$(String[] args) in Program.cs:line 1#include <stdio.h>
enum ShapeKind { CIRCLE, RECT, TRIANGLE }; // TRIANGLE is new!
struct Shape { enum ShapeKind kind; double a, b;};
double area(struct Shape s) { switch (s.kind) { case CIRCLE: return 3.14 * s.a * s.a; case RECT: return s.a * s.b; } return 0;}
int main(void) { struct Shape t = {TRIANGLE, 2.0, 3.0}; printf("%g\n", area(t)); return 0;}main.c: In function ‘area’:
main.c:11:5: warning: enumeration value ‘TRIANGLE’ not handled in switch [-Wswitch]
11 | switch (s.kind) {
| ^~~~~~0
C++ refuses this too when you use std::visit: no lambda accepts a Triangle, so it fails to compile, with a long template error. A plain enum plus switch would only get a -Wswitch warning.
#include <iostream>#include <variant>
struct Circle { double r; };struct Rect { double w, h; };struct Triangle { double b, h; }; // new!using Shape = std::variant<Circle, Rect, Triangle>;
template <class... Fs> struct overloaded : Fs... { using Fs::operator()...; };
double area(const Shape& s) { return std::visit(overloaded{ [](const Circle& c) { return 3.14 * c.r * c.r; }, [](const Rect& r) { return r.w * r.h; }, }, s);}
int main() { std::cout << area(Triangle{2.0, 3.0}) << "\n";}In file included from /usr/include/c++/14/bits/move.h:37,
from /usr/include/c++/14/bits/exception_ptr.h:41,
from /usr/include/c++/14/exception:166,
from /usr/include/c++/14/ios:41,
from /usr/include/c++/14/ostream:40,
from /usr/include/c++/14/iostream:41,
from main.cpp:1:
/usr/include/c++/14/type_traits: In substitution of ‘template<class _Fn, class ... _Args> using std::invoke_result_t = typename std::invoke_result::type [with _Fn = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Args = {const Triangle&}]’:
/usr/include/c++/14/variant:1131:14: required from ‘constexpr bool std::__detail::__variant::__check_visitor_results(std::index_sequence<_Idx ...>) [with _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variant = const std::variant<Circle, Rect, Triangle>&; long unsigned int ..._Idxs = {0, 1, 2}; std::index_sequence<_Idx ...> = std::integer_sequence<long unsigned int, 0, 1, 2>]’
1131 | return __same_types<
| ^~~~~~~~~~~~~
1132 | invoke_result_t<_Visitor, __get_t<_Idxs, _Variant>>...
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1133 | >;
| ~
/usr/include/c++/14/variant:1872:44: required from ‘constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variants = {const variant<Circle, Rect, Triangle>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = double]’
1871 | constexpr bool __visit_rettypes_match = __detail::__variant::
| ~~~~~~~~~~~~~~~~~~~~~
1872 | __check_visitor_results<_Visitor, _Vp>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
1873 | make_index_sequence<variant_size_v<remove_reference_t<_Vp>>>());
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:12:22: required from here
12 | return std::visit(overloaded{
| ~~~~~~~~~~^~~~~~~~~~~~
13 | [](const Circle& c) { return 3.14 * c.r * c.r; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 | [](const Rect& r) { return r.w * r.h; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15 | }, s);
| ~~~~~
/usr/include/c++/14/type_traits:3170:11: error: no type named ‘type’ in ‘struct std::invoke_result<overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >, const Triangle&>’
3170 | using invoke_result_t = typename invoke_result<_Fn, _Args...>::type;
| ^~~~~~~~~~~~~~~
In file included from main.cpp:2:
/usr/include/c++/14/variant: In instantiation of ‘constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variants = {const variant<Circle, Rect, Triangle>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = double]’:
main.cpp:12:22: required from here
12 | return std::visit(overloaded{
| ~~~~~~~~~~^~~~~~~~~~~~
13 | [](const Circle& c) { return 3.14 * c.r * c.r; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 | [](const Rect& r) { return r.w * r.h; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15 | }, s);
| ~~~~~
/usr/include/c++/14/variant:1872:44: in ‘constexpr’ expansion of ‘std::__detail::__variant::__check_visitor_results<overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >, const std::variant<Circle, Rect, Triangle>&, 0, 1, 2>((std::make_index_sequence<3>(), std::make_index_sequence<3>()))’
/usr/include/c++/14/variant:1872:51: error: ‘constexpr’ call flows off the end of the function
1871 | constexpr bool __visit_rettypes_match = __detail::__variant::
| ~~~~~~~~~~~~~~~~~~~~~
1872 | __check_visitor_results<_Visitor, _Vp>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
1873 | make_index_sequence<variant_size_v<remove_reference_t<_Vp>>>());
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/variant:1876:29: error: non-constant condition for static assertion
1876 | static_assert(__visit_rettypes_match,
| ^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/variant:1879:15: error: return-statement with no value, in function returning ‘std::__detail::__variant::__visit_result_t<overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >, const std::variant<Circle, Rect, Triangle>&>’ {aka ‘double’} [-fpermissive]
1879 | return;
| ^~~~~~
/usr/include/c++/14/variant: In instantiation of ‘static constexpr decltype(auto) std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...)>, std::integer_sequence<long unsigned int, __indices ...> >::__visit_invoke(_Visitor&&, _Variants ...) [with _Result_type = std::__detail::__variant::__deduce_visit_result<double>; _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >&&; _Variants = {const std::variant<Circle, Rect, Triangle>&}; long unsigned int ...__indices = {2}]’:
/usr/include/c++/14/variant:1821:5: required from ‘constexpr decltype(auto) std::__do_visit(_Visitor&&, _Variants&& ...) [with _Result_type = __detail::__variant::__deduce_visit_result<double>; _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variants = {const variant<Circle, Rect, Triangle>&}]’
1821 | _GLIBCXX_VISIT_CASE(2)
| ^~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/variant:1882:34: required from ‘constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variants = {const variant<Circle, Rect, Triangle>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = double]’
1882 | return std::__do_visit<_Tag>(
| ~~~~~~~~~~~~~~~~~~~~~^
1883 | std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1884 | static_cast<_Vp>(__variants)...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:12:22: required from here
12 | return std::visit(overloaded{
| ~~~~~~~~~~^~~~~~~~~~~~
13 | [](const Circle& c) { return 3.14 * c.r * c.r; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 | [](const Rect& r) { return r.w * r.h; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15 | }, s);
| ~~~~~
/usr/include/c++/14/variant:1060:31: error: no matching function for call to ‘__invoke(overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >, const Triangle&)’
1060 | return std::__invoke(std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1061 | __element_by_index_or_cookie<__indices>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1062 | std::forward<_Variants>(__vars))...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/c++/14/bits/refwrap.h:38,
from /usr/include/c++/14/string:52,
from /usr/include/c++/14/bits/locale_classes.h:40,
from /usr/include/c++/14/bits/ios_base.h:41,
from /usr/include/c++/14/ios:44:
/usr/include/c++/14/bits/invoke.h:90:5: note: candidate: ‘template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...)’
90 | __invoke(_Callable&& __fn, _Args&&... __args)
| ^~~~~~~~
/usr/include/c++/14/bits/invoke.h:90:5: note: template argument deduction/substitution failed:
/usr/include/c++/14/bits/invoke.h: In substitution of ‘template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...) [with _Callable = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Args = {const Triangle&}]’:
/usr/include/c++/14/variant:1060:24: required from ‘static constexpr decltype(auto) std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...)>, std::integer_sequence<long unsigned int, __indices ...> >::__visit_invoke(_Visitor&&, _Variants ...) [with _Result_type = std::__detail::__variant::__deduce_visit_result<double>; _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >&&; _Variants = {const std::variant<Circle, Rect, Triangle>&}; long unsigned int ...__indices = {2}]’
1060 | return std::__invoke(std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1061 | __element_by_index_or_cookie<__indices>(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1062 | std::forward<_Variants>(__vars))...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/variant:1821:5: required from ‘constexpr decltype(auto) std::__do_visit(_Visitor&&, _Variants&& ...) [with _Result_type = __detail::__variant::__deduce_visit_result<double>; _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variants = {const variant<Circle, Rect, Triangle>&}]’
1821 | _GLIBCXX_VISIT_CASE(2)
| ^~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/variant:1882:34: required from ‘constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >; _Variants = {const variant<Circle, Rect, Triangle>&}; __detail::__variant::__visit_result_t<_Visitor, _Variants ...> = double]’
1882 | return std::__do_visit<_Tag>(
| ~~~~~~~~~~~~~~~~~~~~~^
1883 | std::forward<_Visitor>(__visitor),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1884 | static_cast<_Vp>(__variants)...);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:12:22: required from here
12 | return std::visit(overloaded{
| ~~~~~~~~~~^~~~~~~~~~~~
13 | [](const Circle& c) { return 3.14 * c.r * c.r; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 | [](const Rect& r) { return r.w * r.h; },
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15 | }, s);
| ~~~~~
/usr/include/c++/14/bits/invoke.h:90:5: error: no type named ‘type’ in ‘struct std::__invoke_result<overloaded<area(const Shape&)::<lambda(const Circle&)>, area(const Shape&)::<lambda(const Rect&)> >, const Triangle&>’
90 | __invoke(_Callable&& __fn, _Args&&... __args)
| ^~~~~~~~What does the compiler say? Guess first, then open.
error[E0004]: non-exhaustive patterns: `&Shape::Triangle(_, _)` not covered --> src/main.rs:8:11 | 8 | match s { | ^ pattern `&Shape::Triangle(_, _)` not covered | note: `Shape` defined here --> src/main.rs:1:6 | 1 | enum Shape { | ^^^^^ ... 4 | Triangle(f64, f64), // new! | -------- not covered = note: the matched value is of type `&Shape` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 10 ~ Shape::Rect(w, h) => w * h, 11 ~ &Shape::Triangle(_, _) => todo!(),
Go runs this and prints 0, which is wrong. The compiler does not check that a type switch is complete, so Triangle falls through to return 0 with no warning. A default: that panics would make it loud, if you remember to write one.
Kotlin refuses this too: a when over a sealed class must cover every subclass, so adding Triangle breaks the build until you handle it. An else -> branch would silence the check.
Python prints None. No case matches the new Triangle, so area falls off the end and returns None instead of a number. Python itself gives no warning.
TypeScript refuses this too, because the default branch assigns the leftover value to never and a triangle is not never (TS2322). Without that default line, a function that returns a number still fails under strict (TS2366), but a function that returns nothing compiles, and the missing case goes unnoticed.
Java refuses this too: a switch expression over a sealed interface must cover every permitted type, so adding Triangle breaks the build until you handle it. The javac message does not name Triangle; Rust's E0004 does.
The compiler only warns that the switch is not exhaustive (CS8509). It cannot name Triangle, because a record hierarchy is not a closed list. The program builds, then throws a SwitchExpressionException when a Triangle comes in.
gcc warns that TRIANGLE is not handled in the switch, but builds it anyway. The triangle falls through to return 0, so its area is 0.
C++ refuses this too when you use std::visit: no lambda accepts a Triangle, so it fails to compile, with a long template error. A plain enum plus switch would only get a -Wswitch warning.
Rust knows the full list of variants, so it checks every match against it.
This is the good kind of error. Add a variant, and the compiler gives you a to-do list of every place that needs updating.
The fix
Compiles and runs.
enum Shape { Circle(f64), Rect(f64, f64), Triangle(f64, f64),}
fn area(s: &Shape) -> f64 { match s { Shape::Circle(r) => 3.14 * r * r, Shape::Rect(w, h) => w * h, Shape::Triangle(b, h) => 0.5 * b * h, }}
fn main() { let shapes = [ Shape::Circle(1.0), Shape::Rect(2.0, 3.0), Shape::Triangle(2.0, 3.0), ]; for s in &shapes { println!("{}", area(s)); }}3.14 6 3
The toolkit
An enum is one of a fixed list
An enum (short for enumeration) is a type whose value is exactly one of a list of names, called variants. You write the list once. The compiler then knows every value the type can hold, which is what lets it check your code.
#[derive(Debug)]enum Direction { North, East, South, West,}
fn turn_right(d: Direction) -> Direction { match d { Direction::North => Direction::East, Direction::East => Direction::South, Direction::South => Direction::West, Direction::West => Direction::North, }}
fn main() { let heading = Direction::North; println!("start: {:?}", heading); let heading = turn_right(heading); println!("after one turn: {:?}", heading);}start: North after one turn: East
- A variant is written
Direction::North: the enum’s name,::, the variant’s name. matchtakes a value apart by variant (match lesson).#[derive(Debug)]lets{:?}print the value. More onderivebelow.
Variants that carry data
This is where Rust enums go further than the enums in most languages. Each variant can hold its own data, and different variants can hold different things.
#[derive(Debug)]enum Command { Quit, // no data Say(String), // one value Paint(u8, u8, u8), // three values, by position Move { x: i32, y: i32 }, // named fields, like a struct}
fn run(cmd: Command) { match cmd { Command::Quit => println!("quitting"), Command::Say(text) => println!("saying {text:?}"), Command::Paint(r, g, b) => println!("painting rgb({r}, {g}, {b})"), Command::Move { x, y } => println!("moving by ({x}, {y})"), }}
fn main() { let cmds = vec![ Command::Say(String::from("hi")), Command::Paint(255, 128, 0), Command::Move { x: 3, y: -4 }, Command::Quit, ]; println!("debug: {:?}", cmds[1]); for cmd in cmds { run(cmd); }}debug: Paint(255, 128, 0) saying "hi" painting rgb(255, 128, 0) moving by (3, -4) quitting
There are three shapes of variant, and one enum can mix them:
| Shape | Written | Fields are reached by |
|---|---|---|
| No data | Quit |
nothing to reach |
| Tuple-like | Paint(u8, u8, u8) |
position: Command::Paint(r, g, b) |
| Struct-like | Move { x: i32, y: i32 } |
name: Command::Move { x, y } |
A value is exactly one variant at a time. A Command is never a Say and a Move together, and you cannot read x from a Say. match is how you ask which one you have, and it hands you the data in the same step.
A variant is not a type
Command::Say is a value, not a type. A function can take a Command, but it cannot ask for “only a Say”:
enum Shape { Circle(f64), Rect(f64, f64),}
// Circle is a variant of Shape, not a type of its ownfn radius(c: Shape::Circle) -> f64 { c.0}
fn main() { let _ = Shape::Rect(2.0, 3.0); println!("{}", radius(Shape::Circle(1.0)));}error[E0573]: expected type, found variant `Shape::Circle` --> src/main.rs:7:14 | 7 | fn radius(c: Shape::Circle) -> f64 { | ^^^^^^^^^^^^^ not a type | help: try using the variant's enum | 7 - fn radius(c: Shape::Circle) -> f64 { 7 + fn radius(c: crate::Shape) -> f64 {
If you want a function that accepts only circles, make Circle a small struct and let the variant hold it: Shape::Circle(Circle). Then fn radius(c: &Circle) works, and Shape still lists every case.
Methods on enums
An enum takes an impl block like a struct does (see Methods). Putting the match inside a method keeps the case analysis in one place.
enum Shape { Circle { radius: f64 }, Rect { width: f64, height: f64 }, Triangle { base: f64, height: f64 },}
impl Shape { fn area(&self) -> f64 { match self { Shape::Circle { radius } => 3.14 * radius * radius, Shape::Rect { width, height } => width * height, Shape::Triangle { base, height } => 0.5 * base * height, } }
fn name(&self) -> &str { match self { Shape::Circle { .. } => "circle", Shape::Rect { .. } => "rectangle", Shape::Triangle { .. } => "triangle", } }
// an "associated function", called as Shape::square(2.0) fn square(side: f64) -> Shape { Shape::Rect { width: side, height: side, } }}
fn main() { let shapes = [ Shape::Circle { radius: 1.0 }, Shape::square(2.0), Shape::Triangle { base: 3.0, height: 4.0, }, ]; for s in &shapes { println!("{}: area {}", s.name(), s.area()); }}circle: area 3.14 rectangle: area 4 triangle: area 6
Shape::square(2.0) is an associated function: it has no self, and it builds a value. Shape::Circle { .. } in name means “a circle, whatever its fields”.
match must cover every variant
Leave a variant out and the program does not compile:
enum Shape { Circle(f64), Rect(f64, f64), Triangle(f64, f64),}
fn area(s: &Shape) -> f64 { match s { Shape::Circle(r) => 3.14 * r * r, Shape::Rect(w, h) => w * h, }}
fn main() { println!("{}", area(&Shape::Triangle(3.0, 4.0)));}error[E0004]: non-exhaustive patterns: `&Shape::Triangle(_, _)` not covered --> src/main.rs:8:11 | 8 | match s { | ^ pattern `&Shape::Triangle(_, _)` not covered | note: `Shape` defined here --> src/main.rs:1:6 | 1 | enum Shape { | ^^^^^ ... 4 | Triangle(f64, f64), | -------- not covered = note: the matched value is of type `&Shape` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 10 ~ Shape::Rect(w, h) => w * h, 11 ~ &Shape::Triangle(_, _) => todo!(),
The error is E0004. It names the missing case, points at the variant, and suggests an arm. When you add a variant to an enum later, every match that forgot it fails the same way. The compiler gives you a to-do list of places to update.
Patterns can look inside variants:
enum Shape { Circle { radius: f64 }, Rect { width: f64, height: f64 }, Triangle { base: f64, height: f64 },}
fn describe(s: &Shape) -> String { match s { // a guard: the arm applies only when the condition is true Shape::Circle { radius } if *radius == 0.0 => String::from("a dot"), Shape::Circle { radius } => format!("a circle, radius {radius}"), // a pattern can test a field against a value Shape::Rect { width, height: 1.0 } => format!("a strip, {width} wide"), Shape::Rect { width, height } if width == height => format!("a square, side {width}"), // `..` means "I don't care about the other fields" Shape::Rect { width, .. } => format!("a rectangle, {width} wide"), Shape::Triangle { base, height } => format!("a triangle, {base} by {height}"), }}
fn main() { let shapes = [ Shape::Circle { radius: 0.0 }, Shape::Circle { radius: 2.5 }, Shape::Rect { width: 7.0, height: 1.0, }, Shape::Rect { width: 3.0, height: 3.0, }, Shape::Rect { width: 3.0, height: 4.0, }, Shape::Triangle { base: 1.0, height: 2.0, }, ]; for s in &shapes { println!("{}", describe(s)); }}a dot a circle, radius 2.5 a strip, 7 wide a square, side 3 a rectangle, 3 wide a triangle, 1 by 2
- A guard (
if radius == 0.0) adds a condition to an arm. Arms are tried from the top, so put the specific ones first. height: 1.0inside a pattern requires that field to equal the value...ignores the fields you do not name.
Matching through a reference
The functions above take &Shape, so match gives you references to the fields. In Shape::Circle(r) below, r is a &f64, which is why patterns.rs wrote *radius == 0.0 in a guard. Arithmetic and comparing two references work as they are, and * follows a reference to the number when you need the number itself.
enum Shape { Circle(f64), Rect(f64, f64),}
fn describe(s: &Shape) -> String { match s { // s is a reference, so the fields are references too: r is a &f64 Shape::Circle(r) => { let radius: f64 = *r; // `*` follows the reference to the number format!("circle, radius {radius}") } Shape::Rect(w, h) => { // arithmetic and comparison between two references work as they are let kind = if w == h { "square" } else { "rectangle" }; format!("{kind}, area {}", w * h) } }}
fn main() { println!("{}", describe(&Shape::Circle(1.5))); println!("{}", describe(&Shape::Rect(2.0, 2.0)));}circle, radius 1.5 square, area 4
Matching a value you only borrow, and trying to take something out of it, is the most common first error with enums:
enum Job { Waiting, Failed(String),}
struct Runner { job: Job,}
impl Runner { fn reason(&self) -> String { match self.job { Job::Failed(msg) => msg, // tries to take the String out of &self Job::Waiting => String::new(), } }}
fn main() { let r = Runner { job: Job::Failed(String::from("boom")), }; println!("{}", r.reason()); let _ = Job::Waiting;}error[E0507]: cannot move out of `self.job` as enum variant `Failed` which is behind a shared reference --> src/main.rs:12:15 | 12 | match self.job { | ^^^^^^^^ 13 | Job::Failed(msg) => msg, // tries to take the String out of &self | --- data moved here because `msg` has type `String`, which does not implement the `Copy` trait | help: consider borrowing the pattern binding | 13 | Job::Failed(ref msg) => msg, // tries to take the String out of &self | +++
match self.job tries to move the String out of a &self, and E0507 refuses. Match on &self.job and clone what you need (msg.clone()), or take self by value as the state machine below does.
The _ arm turns the check off
_ matches anything left over. That is handy, but it also matches variants you add next month:
enum Shape { Circle(f64), Rect(f64, f64), Triangle(f64, f64), // added later}
fn area(s: &Shape) -> f64 { match s { Shape::Circle(r) => 3.14 * r * r, Shape::Rect(w, h) => w * h, _ => 0.0, // "everything else": this now includes Triangle }}
// no wildcard: adding Triangle made the compiler send us herefn perimeter(s: &Shape) -> f64 { match s { Shape::Circle(r) => 2.0 * 3.14 * r, Shape::Rect(w, h) => 2.0 * (w + h), Shape::Triangle(a, b) => a + b + (a * a + b * b).sqrt(), // right triangle }}
fn main() { let shapes = [ Shape::Circle(1.0), Shape::Rect(2.0, 3.0), Shape::Triangle(3.0, 4.0), ]; for s in &shapes { println!("area: {}, perimeter: {}", area(s), perimeter(s)); }}area: 3.14, perimeter: 6.28 area: 6, perimeter: 10 area: 0, perimeter: 12
area compiled and printed 0 for the triangle. perimeter had no _, so adding Triangle forced an arm. Use _ when “everything else” really is one case. When you want the compiler to flag new variants, list them.
if let, let else and matches!
When you care about one variant, a full match is a lot of text.
enum Command { Quit, Say(String), Move { x: i32, y: i32 },}
fn shout(cmd: &Command) { // `if let` for the one variant you care about if let Command::Say(text) = cmd { println!("SAY: {}", text.to_uppercase()); } else { println!("not a Say"); }}
fn distance(cmd: &Command) -> i32 { // `let ... else` keeps the happy path unindented let Command::Move { x, y } = cmd else { return 0; }; x.abs() + y.abs()}
fn main() { let a = Command::Say(String::from("hello")); let b = Command::Move { x: 3, y: -4 }; let c = Command::Quit; shout(&a); shout(&b); println!("distance of a: {}", distance(&a)); println!("distance of b: {}", distance(&b)); // `matches!` gives a bool, without binding anything println!("c is Quit: {}", matches!(c, Command::Quit)); println!("b is Move: {}", matches!(b, Command::Move { .. }));}SAY: HELLO not a Say distance of a: 0 distance of b: 7 c is Quit: true b is Move: true
if let PATTERN = value { … }runs the block only if the pattern fits.elseis optional.let PATTERN = value else { … };binds the names for the rest of the block. Theelseblock must leave (return,break,continueor a panic), so the happy path stays unindented.matches!(value, PATTERN)is abool. It binds nothing.
These do not check that you covered everything, so use match when every case matters.
State machines: make illegal states hard to write
Many programs track a thing that moves through stages: a job, an order, a connection. A common first model is one struct whose fields cover every stage:
// One struct: every field exists in every state, so nonsense can be written.struct JobRecord { running: bool, done: bool, percent: u8, // only meaningful while running error: String, // only meaningful if the job failed}
fn main() { let odd = JobRecord { running: true, done: true, percent: 250, error: String::from("boom"), }; println!("running: {}, done: {}", odd.running, odd.done); println!("percent: {}, error: {:?}", odd.percent, odd.error);}running: true, done: true percent: 250, error: "boom"
Nothing stops “running and done” or a percent of 250 on a job that failed. Every function that reads the struct has to guard against nonsense. An enum lists only the states that exist, and each state holds only its own data:
enum Job { Waiting, Running { percent: u8 }, Done { lines: u32 }, Failed(String),}
enum Event { Start, Progress(u8), Finish(u32), Crash(String),}
impl Job { // takes the old state by value and returns the new one fn next(self, event: Event) -> Job { match (self, event) { (Job::Waiting, Event::Start) => Job::Running { percent: 0 }, (Job::Running { .. }, Event::Progress(p)) => Job::Running { percent: p }, (Job::Running { .. }, Event::Finish(lines)) => Job::Done { lines }, (Job::Running { .. }, Event::Crash(why)) => Job::Failed(why), // any other pair is not a legal move: stay where we are (state, _) => state, } }
fn show(&self) -> String { match self { Job::Waiting => String::from("waiting"), Job::Running { percent } => format!("running, {percent}%"), Job::Done { lines } => format!("done, {lines} lines"), Job::Failed(why) => format!("failed: {why}"), } }}
fn main() { let events = [ Event::Progress(50), // too early: the job has not started Event::Start, Event::Progress(50), Event::Finish(120), Event::Crash(String::from("late")), // too late: already done ]; let mut job = Job::Waiting; println!("{}", job.show()); for event in events { job = job.next(event); println!("{}", job.show()); }}waiting waiting running, 0% running, 50% done, 120 lines done, 120 lines
- A
Waitingjob has nopercent, because the field does not exist in that variant. AFailedjob has its reason and nothing else. (Apercentabove 100 is still possible. A smaller type or a checked constructor would fix that.) - Add a
Cancelledvariant later, and everymatchonJobfails to compile until it says whatCancelledmeans. nexttakesselfby value: it consumes the old state and returns the new one, so nobody keeps using a stale state (Ownership). It is the(state, event) -> stateshape of a reducer.- Matching on the pair
(self, event)makes the legal moves a table, one arm per row. The last arm ignores any other move: the firstProgress(50)in the output, on a job that has not started, changed nothing. So the states cannot be wrong, but the moves are checked only by that arm. A later lesson shows returning aResultinstead.
This is called “making illegal states unrepresentable”.
Enums with no data are numbers underneath
An enum whose variants hold no data is called C-like. Each variant has a number (its discriminant), starting at 0 and counting up, or the value you give it. as turns a variant into that number. Only enums where no variant holds data can be cast this way.
#[derive(Debug, Clone, Copy)]enum Level { Low = 1, Mid = 5, High = 10,}
#[derive(Debug, Clone, Copy)]enum Weekday { Mon, // 0 Tue, // 1 Wed = 10, Thu, // 11}
impl Level { // there is no cast from a number to an enum, so write the check yourself fn from_number(n: i32) -> Option<Level> { match n { 1 => Some(Level::Low), 5 => Some(Level::Mid), 10 => Some(Level::High), _ => None, } }}
fn main() { println!("Mid as i32: {}", Level::Mid as i32); println!("High as u8: {}", Level::High as u8); println!("Tue as i32: {}", Weekday::Tue as i32); println!("Wed as i32: {}", Weekday::Wed as i32); println!("Thu as i32: {}", Weekday::Thu as i32); println!("from_number(5): {:?}", Level::from_number(5)); println!("from_number(7): {:?}", Level::from_number(7)); println!("{:?} {:?}", Level::Low, Weekday::Mon);}Mid as i32: 5 High as u8: 10 Tue as i32: 1 Wed as i32: 10 Thu as i32: 11 from_number(5): Some(Mid) from_number(7): None Low Mon
Thu follows Wed = 10, so it is 11. The reverse does not exist. Even 10, which is a real Level, cannot be cast, because Rust does not check numbers at run time:
#[derive(Debug)]enum Level { Low = 1, High = 10,}
fn main() { let n = 10; let level = n as Level; println!("{level:?}");}error[E0605]: non-primitive cast: `i32` as `Level` --> src/main.rs:9:17 | 9 | let level = n as Level; | ^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object
Write a function like from_number above. It returns an Option, a value that is either Some(thing) or None (Option, next lesson). A number attached to each constant is the discriminant (Mid = 5). Other per-constant data goes in a match method.
Variant names as text
{:?} is for programmers (Structs). The text users see comes from Display, which you write yourself. Text coming in needs a function like from_number:
use std::fmt;
#[derive(Debug)]enum Direction { North, South,}
// `impl fmt::Display` is how you say what `{}` prints. Traits come in a later// lesson: for now, copy the shape.impl fmt::Display for Direction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let text = match self { Direction::North => "north", Direction::South => "south", }; write!(f, "{text}") }}
impl Direction { // text in: a name we know, or None fn parse(text: &str) -> Option<Direction> { match text { "north" => Some(Direction::North), "south" => Some(Direction::South), _ => None, } }}
fn main() { let d = Direction::North; println!("Display: {d}"); println!("Debug: {d:?}"); println!("parse(\"south\"): {:?}", Direction::parse("south")); println!("parse(\"up\"): {:?}", Direction::parse("up"));}Display: north
Debug: North
parse("south"): Some(South)
parse("up"): NoneWriting trait impls such as Display is covered in Traits; for now copy the shape. For JSON, the serde crate generates both directions.
derive on enums
Structs explains derive. Nothing is derived unless you ask, so a bare enum can be neither compared nor printed (there is no data class magic):
enum Color { Red, Green,}
fn main() { let c = Color::Red; if c == Color::Green { println!("green"); } println!("{c:?}");}error[E0369]: binary operation `==` cannot be applied to type `Color` --> src/main.rs:8:10 | 8 | if c == Color::Green { | - ^^ ------------ Color | | | Color | note: an implementation of `PartialEq` might be missing for `Color` --> src/main.rs:1:1 | 1 | enum Color { | ^^^^^^^^^^ must implement `PartialEq` help: consider annotating `Color` with `#[derive(PartialEq)]` | 1 + #[derive(PartialEq)] 2 | enum Color { error[E0277]: `Color` doesn't implement `Debug` --> src/main.rs:11:15 | 11 | println!("{c:?}"); | ^^^^^ `Color` cannot be formatted using `{:?}` because it doesn't implement `Debug` | = help: the trait `Debug` is not implemented for `Color` help: consider annotating `Color` with `#[derive(Debug)]` | 1 + #[derive(Debug)] 2 | enum Color { | Some errors have detailed explanations: E0277, E0369.
What the derives mean for an enum:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]enum Priority { Low, #[default] Normal, Urgent,}
#[derive(Debug, Clone, PartialEq)]enum Reply { Ok, Text(String), Number(f64),}
fn main() { let p = Priority::default(); let q = p; // Copy: p is still usable println!("default: {p:?}, copy: {q:?}"); println!("Low < Urgent: {}", Priority::Low < Priority::Urgent); println!("max: {:?}", Priority::Low.max(Priority::Urgent));
let r = Reply::Text(String::from("hi")); println!("clone equal: {}", r.clone() == r); println!( "same variant, other data: {}", r == Reply::Text(String::from("ho")) ); println!("other variant: {}", r == Reply::Ok); println!("{:?} / {:?}", Reply::Number(1.5), Reply::Ok); println!("{:#?}", Reply::Text(String::from("pretty")));}default: Normal, copy: Normal
Low < Urgent: true
max: Urgent
clone equal: true
same variant, other data: false
other variant: false
Number(1.5) / Ok
Text(
"pretty",
)| Derive | On an enum |
|---|---|
PartialEq, Eq |
== is true for the same variant with equal fields |
PartialOrd, Ord |
< and sort compare discriminants first (declaration order, unless you give numbers), then fields |
Hash |
usable as a map or set key, together with Eq (Maps) |
Copy |
possible only if every field is Copy, so any String variant rules it out |
Default |
one unit variant is marked #[default] |
use Enum::*
Typing Direction:: in every arm gets long. A use brings the variant names into scope. Keep it inside the function, so the names do not leak into the rest of the file:
#[derive(Debug)]enum Direction { North, East, South, West,}
fn opposite(d: Direction) -> Direction { // `use` inside the function brings the variant names into scope here only use Direction::*; match d { North => South, South => North, East => West, West => East, }}
fn main() { println!("opposite of East: {:?}", opposite(Direction::East));}opposite of East: West
Forget the use, and a bare name in a pattern is read as a new variable that matches everything. When the name is one of the enum’s variants, Rust catches it:
enum Direction { North, South,}
fn is_north(d: Direction) -> bool { match d { North => true, South => false, }}
fn main() { println!("{}", is_north(Direction::South));}error[E0170]: pattern binding `North` is named the same as one of the variants of the type `Direction` --> src/main.rs:8:9 | 8 | North => true, | ^^^^^ help: to match on the variant, qualify the path: `Direction::North` | = note: `#[deny(bindings_with_variant_name)]` on by default error[E0170]: pattern binding `South` is named the same as one of the variants of the type `Direction` --> src/main.rs:9:9 | 9 | South => false, | ^^^^^ help: to match on the variant, qualify the path: `Direction::South`
Option and Result are enums
Option and Result are not special syntax. They are enums defined in the standard library, and you match them like yours:
// std defines Option and Result as ordinary enums. Here is the same idea by hand.// (`<T>` means "any type here"; generics come in a later lesson.)#[derive(Debug)]enum Maybe<T> { Nothing, Just(T),}
fn first_even(numbers: &[i32]) -> Option<i32> { for &n in numbers { if n % 2 == 0 { return Some(n); } } None}
fn main() { // the real Option: matched like any other enum match first_even(&[1, 3, 4, 5]) { Some(n) => println!("found {n}"), None => println!("no even number"), }
// the real Result: Ok(value) or Err(error) let parsed: Result<i32, _> = "12x".parse::<i32>(); match parsed { Ok(n) => println!("parsed {n}"), Err(e) => println!("could not parse: {e}"), }
// our own copy works the same way let a: Maybe<i32> = Maybe::Just(7); let b: Maybe<i32> = Maybe::Nothing; println!("{a:?} and {b:?}");}found 4 could not parse: invalid digit found in string Just(7) and Nothing
Option<T> is Some(value) or None: a value that may be missing. Result<T, E> is Ok(value) or Err(error): an operation that may fail. Maybe<T> above is the same idea written by hand (<T> means “any type”, covered in Generics). The next two lessons are Option and Errors.
Your enums in Rust
Going deeper
What an enum looks like in memory
Sizes on this page come from Linux on 64-bit ARM with Rust 1.99.1 (C is GCC 14, C++ is g++ 14). They depend on the machine: on a 32-bit target a String is 12 bytes. Where the language promises a size, the text says so.
A variable of an enum type has room for the biggest variant’s data and, usually, a tag that says which variant is stored. The tag is the variant’s discriminant, stored in the value. Every Shape is the same size, whether it holds a circle or a rectangle, because the type has to fit the biggest one.
// This program only measures sizes, so it never builds the variants.#![allow(dead_code)]
use std::mem::{align_of, size_of};
enum Plain { A, B, C,}
enum Small { Byte(u8), Nothing,}
enum Shape { Circle(f64), Rect(f64, f64),}
enum Command { Quit, Say(String), Paint(u8, u8, u8),}
enum Big { Quit, Say(String), Paint(u64, u64, u64),}
fn main() { println!("Plain (3 variants, no data): {} byte", size_of::<Plain>()); println!("Small (u8 or nothing): {} bytes", size_of::<Small>()); println!("Shape (f64, or two f64): {} bytes", size_of::<Shape>()); println!( " its biggest variant data: {} bytes", size_of::<(f64, f64)>() ); println!(" its alignment: {} bytes", align_of::<Shape>()); println!( "Command (String, 3 u8, none): {} bytes", size_of::<Command>() ); println!(" a String alone: {} bytes", size_of::<String>()); println!("Big (String, 3 u64, none): {} bytes", size_of::<Big>());}Plain (3 variants, no data): 1 byte Small (u8 or nothing): 2 bytes Shape (f64, or two f64): 24 bytes its biggest variant data: 16 bytes its alignment: 8 bytes Command (String, 3 u8, none): 24 bytes a String alone: 24 bytes Big (String, 3 u64, none): 32 bytes
On this machine Shape is 24 bytes: a circle needs one f64 (8 bytes), a rectangle two (16), and the tag is padded out to 8 so the f64s start at multiples of 8. Plain, with three variants and no data, is a single byte: only the tag. These are rustc’s choices, because the Reference leaves the default layout open. For a picture we ask for a fixed layout with #[repr(u8)]: a one-byte tag first, then the variant’s fields. We can read the bytes:
// The fields are read through raw pointers, which the compiler cannot see.#![allow(dead_code)]
// With #[repr(u8)] the layout is fixed by the language: a one-byte tag first,// then the variant's fields. That lets us look at the bytes.#[repr(u8)]enum Shape { Circle(f64), Rect(f64, f64),}
fn tag(s: &Shape) -> u8 { // unsafe: we promise that byte 0 of a Shape is inside the value (it is) unsafe { *(s as *const Shape as *const u8) }}
fn f64_at(s: &Shape, offset: usize) -> f64 { // unsafe: we promise that there is an f64 stored at this offset unsafe { *((s as *const Shape as *const u8).add(offset) as *const f64) }}
fn main() { println!("size of Shape: {} bytes", size_of::<Shape>());
let circle = Shape::Circle(1.0); println!( "Circle(1.0): tag {}, f64 at byte 8: {}", tag(&circle), f64_at(&circle, 8) );
let rect = Shape::Rect(2.0, 3.0); println!( "Rect(2.0, 3.0): tag {}, f64 at byte 8: {}, f64 at byte 16: {}", tag(&rect), f64_at(&rect, 8), f64_at(&rect, 16) );}size of Shape: 24 bytes Circle(1.0): tag 0, f64 at byte 8: 1 Rect(2.0, 3.0): tag 1, f64 at byte 8: 2, f64 at byte 16: 3
(unsafe means “the compiler cannot check this, I promise it is fine”. Here it is needed only because we read raw bytes.)
repr(u8), not for a plain enum.Command holds a String, which is 24 bytes on its own, and Command is also 24 bytes on this machine. A String has no padding, so here the tag must live inside the String’s own bytes, in values a real String never has. That works only because the other variants (three u8s) fit in the remaining bytes. Big, with three u64s, is 32. Nothing in the language promises either result: the only sizes the standard library guarantees are the Option cases below.
Niches: why Option is often free
A u32 has no spare value to mean “none”, so Option<u32> is 8 bytes. A reference is never null, so the value 0 is free to mean None. The compiler uses it, and the tag disappears. A free bit pattern like this is a niche.
use std::mem::{size_of, transmute};use std::num::NonZeroU32;
fn main() { println!("--- Option costs nothing extra here ---"); println!("&u8: {}", size_of::<&u8>()); println!("Option<&u8>: {}", size_of::<Option<&u8>>()); println!("Box<i32>: {}", size_of::<Box<i32>>()); println!("Option<Box<i32>>: {}", size_of::<Option<Box<i32>>>()); println!("NonZeroU32: {}", size_of::<NonZeroU32>()); println!("Option<NonZeroU32>: {}", size_of::<Option<NonZeroU32>>()); println!("bool: {}", size_of::<bool>()); println!("Option<bool>: {}", size_of::<Option<bool>>()); println!("String: {}", size_of::<String>()); println!("Option<String>: {}", size_of::<Option<String>>());
println!("--- Here it does, because every bit pattern is a real value ---"); println!("u32: {}", size_of::<u32>()); println!("Option<u32>: {}", size_of::<Option<u32>>()); println!("u64: {}", size_of::<u64>()); println!("Option<u64>: {}", size_of::<Option<u64>>());
println!("--- What None looks like ---"); let value = 7u8; let none: Option<&u8> = None; let some: Option<&u8> = Some(&value); // unsafe: reinterpret the 8 bytes of each Option as a number, only to look at them let none_bits: usize = unsafe { transmute(none) }; let some_bits: usize = unsafe { transmute(some) }; println!("None as a number: {none_bits}"); println!("Some as a number is not zero: {}", some_bits != 0);}--- Option costs nothing extra here --- &u8: 8 Option<&u8>: 8 Box<i32>: 8 Option<Box<i32>>: 8 NonZeroU32: 4 Option<NonZeroU32>: 4 bool: 1 Option<bool>: 1 String: 24 Option<String>: 24 --- Here it does, because every bit pattern is a real value --- u32: 4 Option<u32>: 8 u64: 8 Option<u64>: 16 --- What None looks like --- None as a number: 0 Some as a number is not zero: true
Option<&u8> is 8 bytes, the same as &u8. There is no separate tag. The program above printed it: None reads as 0 and Some as non-zero.The standard library guarantees this for Option of Box<T>, &T, &mut T (with T of known size), function pointers and the NonZero number types. Other types, such as bool and String, have niches too, as the output shows, but only the types in the docs are promised. Option<&T> is the safe replacement for a pointer that might be null, and it is the same size as the pointer.
Recursive enums need Box
A tree is an enum that contains itself: an expression is a number, or two expressions added together. Written directly, the size has no end, because Expr would hold two Exprs, each holding two more:
enum Expr { Num(i64), Add(Expr, Expr),}
fn main() { let _ = Expr::Num(1);}error[E0072]: recursive type `Expr` has infinite size --> src/main.rs:1:1 | 1 | enum Expr { | ^^^^^^^^^ 2 | Num(i64), 3 | Add(Expr, Expr), | ---- recursive without indirection | help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle | 3 | Add(Box<Expr>, Expr), | ++++ +
The fix is Box<Expr>. A Box is a pointer to a value on the heap (the pool of memory from Stack and heap). The pointer has a fixed size, whatever it points at, so Expr has a size again. Boxes get a full lesson in Smart pointers. For now you only need Box::new(value) to make one. A &Box<Expr> can be passed where a &Expr is expected, which is why eval(a) works below.
use std::mem::size_of;
enum Expr { Num(i64), Add(Box<Expr>, Box<Expr>), // a Box is a pointer to the heap Mul(Box<Expr>, Box<Expr>),}
fn eval(e: &Expr) -> i64 { match e { Expr::Num(n) => *n, Expr::Add(a, b) => eval(a) + eval(b), Expr::Mul(a, b) => eval(a) * eval(b), }}
fn main() { // (1 + 2) * 4 let e = Expr::Mul( Box::new(Expr::Add(Box::new(Expr::Num(1)), Box::new(Expr::Num(2)))), Box::new(Expr::Num(4)), ); println!("(1 + 2) * 4 = {}", eval(&e)); println!("size of Box<Expr>: {} bytes", size_of::<Box<Expr>>()); println!("size of Expr: {} bytes", size_of::<Expr>());}(1 + 2) * 4 = 12 size of Box<Expr>: 8 bytes size of Expr: 24 bytes
How other languages handle this
| Closed list with data | Missing case is | |
|---|---|---|
| Rust | enum |
a compile error |
| Kotlin | sealed class |
a compile error for when (not with else ->) |
| Java | sealed interface and records |
a compile error for switch (not with default ->) |
| C# on .NET 10 | records (not closed) | a warning (CS8509) and a run-time exception |
| TypeScript | union with a kind field |
an error with the never check, or under strict if a return is missing |
| Go | interface and type switch | nothing: it falls through |
| Python | dataclasses and match |
nothing: it falls through |
| C | tag and union |
a warning from gcc -Wall |
| C++ | std::variant |
a compile error with std::visit |
Most of these rows are runs you can see in the gotcha at the top of the page, under each language’s tab.
Practice
Try it
Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed.
Areas of shapes
Fill in area for a circle, a rectangle and a triangle given by its three sides (Heron’s formula). A set of sides that cannot make a triangle must give 0, not NaN.
enum Shape { Circle { radius: f64 }, Rect { width: f64, height: f64 }, Triangle { a: f64, b: f64, c: f64 }, // the three side lengths}
impl Shape { /// Circle: 3.14 * r * r. Rect: width * height. /// Triangle: Heron's formula, sqrt(s * (s - a) * (s - b) * (s - c)) with /// s = (a + b + c) / 2. If the sides cannot make a triangle (one side is /// at least as long as the other two together), the area is 0.0. fn area(&self) -> f64 { todo!("match on self, one arm per variant") }}
fn close(x: f64, y: f64) -> bool { (x - y).abs() < 1e-9}
fn main() { assert!(close(Shape::Circle { radius: 1.0 }.area(), 3.14)); assert!(close(Shape::Circle { radius: 0.0 }.area(), 0.0)); assert!(close( Shape::Rect { width: 2.0, height: 3.0 } .area(), 6.0 )); assert!(close( Shape::Triangle { a: 3.0, b: 4.0, c: 5.0 } .area(), 6.0 )); // sides that cannot make a triangle assert!(close( Shape::Triangle { a: 1.0, b: 2.0, c: 3.0 } .area(), 0.0 )); assert!(close( Shape::Triangle { a: 1.0, b: 1.0, c: 5.0 } .area(), 0.0 )); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
enum Shape { Circle { radius: f64 }, Rect { width: f64, height: f64 }, Triangle { a: f64, b: f64, c: f64 }, // the three side lengths}
impl Shape { /// Circle: 3.14 * r * r. Rect: width * height. /// Triangle: Heron's formula, sqrt(s * (s - a) * (s - b) * (s - c)) with /// s = (a + b + c) / 2. If the sides cannot make a triangle (one side is /// at least as long as the other two together), the area is 0.0. fn area(&self) -> f64 { match self { Shape::Circle { radius } => 3.14 * radius * radius, Shape::Rect { width, height } => width * height, Shape::Triangle { a, b, c } => { // without this check, sqrt of a negative number would give NaN if a + b <= *c || a + c <= *b || b + c <= *a { return 0.0; } let s = (a + b + c) / 2.0; (s * (s - a) * (s - b) * (s - c)).sqrt() } } }}
fn close(x: f64, y: f64) -> bool { (x - y).abs() < 1e-9}
fn main() { assert!(close(Shape::Circle { radius: 1.0 }.area(), 3.14)); assert!(close(Shape::Circle { radius: 0.0 }.area(), 0.0)); assert!(close( Shape::Rect { width: 2.0, height: 3.0 } .area(), 6.0 )); assert!(close( Shape::Triangle { a: 3.0, b: 4.0, c: 5.0 } .area(), 6.0 )); // sides that cannot make a triangle assert!(close( Shape::Triangle { a: 1.0, b: 2.0, c: 3.0 } .area(), 0.0 )); assert!(close( Shape::Triangle { a: 1.0, b: 1.0, c: 5.0 } .area(), 0.0 )); println!("all checks passed");}all checks passed
A door with a lock
Write the apply function for a door that is open, closed or locked with a code. Moves the rules do not allow, such as locking an open door or unlocking with the wrong code, leave the door as it is. apply takes the old door by value, so a match on the pair can hand it back.
#[derive(Debug, PartialEq)]enum Door { Open, Closed, Locked(u32), // the code that locks it}
enum Action { Open, Close, Lock(u32), // lock with this code Unlock(u32), // try to unlock with this code}
/// The rules:/// Closed + Open -> Open/// Open + Close -> Closed/// Closed + Lock(code) -> Locked(code)/// Locked(code) + Unlock(same code) -> Closed/// Anything else (an open door cannot be locked, a locked door cannot be/// opened, a wrong code does nothing) leaves the door as it is.fn apply(door: Door, action: Action) -> Door { todo!("match on the pair (door, action)")}
fn main() { assert_eq!(apply(Door::Closed, Action::Open), Door::Open); assert_eq!(apply(Door::Open, Action::Close), Door::Closed); assert_eq!(apply(Door::Closed, Action::Lock(1234)), Door::Locked(1234)); assert_eq!( apply(Door::Locked(1234), Action::Unlock(1234)), Door::Closed ); // moves that are not allowed assert_eq!(apply(Door::Open, Action::Lock(1)), Door::Open); assert_eq!(apply(Door::Locked(7), Action::Open), Door::Locked(7)); assert_eq!(apply(Door::Locked(7), Action::Unlock(8)), Door::Locked(7)); assert_eq!(apply(Door::Closed, Action::Unlock(7)), Door::Closed); // a whole story let mut door = Door::Open; for action in [ Action::Close, Action::Lock(42), Action::Open, Action::Unlock(42), Action::Open, ] { door = apply(door, action); } assert_eq!(door, Door::Open); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
#[derive(Debug, PartialEq)]enum Door { Open, Closed, Locked(u32), // the code that locks it}
enum Action { Open, Close, Lock(u32), // lock with this code Unlock(u32), // try to unlock with this code}
/// The rules:/// Closed + Open -> Open/// Open + Close -> Closed/// Closed + Lock(code) -> Locked(code)/// Locked(code) + Unlock(same code) -> Closed/// Anything else (an open door cannot be locked, a locked door cannot be/// opened, a wrong code does nothing) leaves the door as it is.fn apply(door: Door, action: Action) -> Door { match (door, action) { (Door::Closed, Action::Open) => Door::Open, (Door::Open, Action::Close) => Door::Closed, (Door::Closed, Action::Lock(code)) => Door::Locked(code), (Door::Locked(code), Action::Unlock(tried)) if code == tried => Door::Closed, (door, _) => door, }}
fn main() { assert_eq!(apply(Door::Closed, Action::Open), Door::Open); assert_eq!(apply(Door::Open, Action::Close), Door::Closed); assert_eq!(apply(Door::Closed, Action::Lock(1234)), Door::Locked(1234)); assert_eq!( apply(Door::Locked(1234), Action::Unlock(1234)), Door::Closed ); // moves that are not allowed assert_eq!(apply(Door::Open, Action::Lock(1)), Door::Open); assert_eq!(apply(Door::Locked(7), Action::Open), Door::Locked(7)); assert_eq!(apply(Door::Locked(7), Action::Unlock(8)), Door::Locked(7)); assert_eq!(apply(Door::Closed, Action::Unlock(7)), Door::Closed); // a whole story let mut door = Door::Open; for action in [ Action::Close, Action::Lock(42), Action::Open, Action::Unlock(42), Action::Open, ] { door = apply(door, action); } assert_eq!(door, Door::Open); println!("all checks passed");}all checks passed
Evaluate and print an expression
Expr is a tree of numbers, negations, sums and products. Write eval, which computes it, and show, which prints it with brackets. Both call themselves on the Boxed parts: pass a as it is, because a &Box<Expr> converts to &Expr by itself.
enum Expr { Num(i64), Neg(Box<Expr>), Add(Box<Expr>, Box<Expr>), Mul(Box<Expr>, Box<Expr>),}
use Expr::*;
fn num(n: i64) -> Box<Expr> { Box::new(Num(n))}
/// Num(5) is 5. Neg(x) is minus x. Add and Mul work as you expect.fn eval(e: &Expr) -> i64 { todo!("one arm per variant; the Boxed parts are evaluated by calling eval again")}
/// Fully bracketed text: Num(5) -> "5", Neg(x) -> "-x",/// Add(a, b) -> "(a + b)", Mul(a, b) -> "(a * b)".fn show(e: &Expr) -> String { todo!("same shape as eval, but build a String")}
fn main() { // (1 + 2) * 4 let e = Mul(Box::new(Add(num(1), num(2))), num(4)); assert_eq!(eval(&e), 12); assert_eq!(show(&e), "((1 + 2) * 4)"); // a lone number, and a negative one assert_eq!(eval(&Num(5)), 5); assert_eq!(show(&Num(-5)), "-5"); // Neg of Neg, with a negative number inside let n = Neg(Box::new(Neg(num(-3)))); assert_eq!(eval(&n), -3); assert_eq!(show(&n), "---3"); // deep on the left: ((((0 + 1) + 2) + 3) + 4) let mut deep = Num(0); for i in 1..=4 { deep = Add(Box::new(deep), num(i)); } assert_eq!(eval(&deep), 10); assert_eq!(show(&deep), "((((0 + 1) + 2) + 3) + 4)"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
enum Expr { Num(i64), Neg(Box<Expr>), Add(Box<Expr>, Box<Expr>), Mul(Box<Expr>, Box<Expr>),}
use Expr::*;
fn num(n: i64) -> Box<Expr> { Box::new(Num(n))}
/// Num(5) is 5. Neg(x) is minus x. Add and Mul work as you expect.fn eval(e: &Expr) -> i64 { match e { Num(n) => *n, Neg(x) => -eval(x), Add(a, b) => eval(a) + eval(b), Mul(a, b) => eval(a) * eval(b), }}
/// Fully bracketed text: Num(5) -> "5", Neg(x) -> "-x",/// Add(a, b) -> "(a + b)", Mul(a, b) -> "(a * b)".fn show(e: &Expr) -> String { match e { Num(n) => n.to_string(), Neg(x) => format!("-{}", show(x)), Add(a, b) => format!("({} + {})", show(a), show(b)), Mul(a, b) => format!("({} * {})", show(a), show(b)), }}
fn main() { // (1 + 2) * 4 let e = Mul(Box::new(Add(num(1), num(2))), num(4)); assert_eq!(eval(&e), 12); assert_eq!(show(&e), "((1 + 2) * 4)"); // a lone number, and a negative one assert_eq!(eval(&Num(5)), 5); assert_eq!(show(&Num(-5)), "-5"); // Neg of Neg, with a negative number inside let n = Neg(Box::new(Neg(num(-3)))); assert_eq!(eval(&n), -3); assert_eq!(show(&n), "---3"); // deep on the left: ((((0 + 1) + 2) + 3) + 4) let mut deep = Num(0); for i in 1..=4 { deep = Add(Box::new(deep), num(i)); } assert_eq!(eval(&deep), 10); assert_eq!(show(&deep), "((((0 + 1) + 2) + 3) + 4)"); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 6: Enums and Pattern Matching, with defining an enum,
matchandif letandlet…else. - The Rust Book: using
Box<T>, which covers recursive types. - Rust by Example: Enums, including
useand C-like enums. - The Reference: Enumerations and Type layout.
std::option(see “Representation”),Result,std::mem::size_ofand thematches!macro.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.