Lesson 21 of 94
Enums: Types With Variants
A Rust enum is a fixed set of variants, each of which can carry its own data. That makes enums one of the most useful features in the language.
| Concept | Rust |
|---|---|
| Named constants | enum Direction { North, South } |
| Variants carrying data | enum Shape { Circle(f64), Square(f64) } |
| Branching on a variant | match (exhaustive, checked at compile time) |
| A value that may be absent | Option<T>, itself an enum: next lesson |
| A value that may be an error | Result<T, E>, also an enum: Result |
1. A simple enum
Variants use PascalCase, and you refer to one with EnumName::Variant.
enum Direction {
North,
East,
South,
West,
}
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "up",
Direction::East => "right",
Direction::South => "down",
Direction::West => "left",
}
}
fn main() {
println!("{}", describe(Direction::North));
println!("{}", describe(Direction::West));
}up
left2. Deriving common behavior
Like structs, enums get debug printing, copying, and comparison through derive.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Level {
Debug,
Info,
Warn,
Error,
}
fn main() {
let configured = Level::Warn;
let incoming = Level::Error;
println!("{:?} vs {:?}", configured, incoming);
println!("same level: {}", configured == incoming);
let copy = configured; // Copy: `configured` stays usable
println!("{:?} {:?}", copy, configured);
}Warn vs Error
same level: false
Warn Warn3. match must handle every variant
This is the most important property of enums. Forget a variant and the code does not compile.
enum PaymentStatus {
Pending,
Authorized,
Captured,
Refunded,
}
fn label(s: PaymentStatus) -> &'static str {
match s {
PaymentStatus::Pending => "pending",
PaymentStatus::Authorized => "authorized",
PaymentStatus::Captured => "captured",
}
}
fn main() {
println!("{}", label(PaymentStatus::Refunded));
}error[E0004]: non-exhaustive patterns: `PaymentStatus::Refunded` not covered
--> src/main.rs:9:11
|
9 | match s {
| ^ pattern `PaymentStatus::Refunded` not covered
|
note: `PaymentStatus` defined here
--> src/main.rs:1:6
|
1 | enum PaymentStatus {
| ^^^^^^^^^^^^^
...
5 | Refunded,
| -------- not covered
= note: the matched value is of type `PaymentStatus`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
12 ~ PaymentStatus::Captured => "captured",
13 ~ PaymentStatus::Refunded => todo!(),
|When a teammate adds a new variant, every match that needs updating becomes a compile error pointing at the exact line.
4. use to shorten variant names
use Enum::* brings the variants into scope so you can write them without the prefix. It is common inside a match on one enum.
#[derive(Debug, Clone, Copy)]
enum Suit {
Hearts,
Diamonds,
Clubs,
Spades,
}
fn color(s: Suit) -> &'static str {
use Suit::*;
match s {
Hearts | Diamonds => "red",
Clubs | Spades => "black",
}
}
fn main() {
for s in [Suit::Hearts, Suit::Clubs, Suit::Diamonds, Suit::Spades] {
println!("{:?} is {}", s, color(s));
}
}Hearts is red
Clubs is black
Diamonds is red
Spades is black5. Explicit numeric values
A variant without data can have an explicit integer value, and as converts it to that number, and you control the numbers.
#[derive(Debug, Clone, Copy)]
enum HttpStatus {
Ok = 200,
NotFound = 404,
InternalError = 500,
}
fn main() {
let s = HttpStatus::NotFound;
println!("{:?} = {}", s, s as u16);
println!("{}", HttpStatus::Ok as i32 + 1);
}NotFound = 404
201Converting a number back to an enum can fail (what is 418?), so Rust has no automatic reverse cast. You write it yourself, as in example 7.
6. Methods on enums
Enums get impl blocks exactly like structs.
#[derive(Debug, Clone, Copy, PartialEq)]
enum TrafficLight {
Red,
Green,
Yellow,
}
impl TrafficLight {
fn next(self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
fn duration_secs(self) -> u32 {
match self {
TrafficLight::Red => 30,
TrafficLight::Green => 25,
TrafficLight::Yellow => 5,
}
}
}
fn main() {
let mut light = TrafficLight::Red;
for _ in 0..4 {
println!("{:?} for {}s", light, light.duration_secs());
light = light.next();
}
}Red for 30s
Green for 25s
Yellow for 5s
Red for 30s7. Converting from a number or string
An associated function builds a variant from outside data. Here unknown input maps to a catch-all variant; the next lesson shows the better tool, Option.
#[derive(Debug)]
enum Weekday {
Mon, Tue, Wed, Thu, Fri, Sat, Sun,
Invalid,
}
impl Weekday {
fn from_number(n: u8) -> Self {
match n {
1 => Self::Mon,
2 => Self::Tue,
3 => Self::Wed,
4 => Self::Thu,
5 => Self::Fri,
6 => Self::Sat,
7 => Self::Sun,
_ => Self::Invalid,
}
}
fn is_weekend(&self) -> bool {
matches!(self, Self::Sat | Self::Sun)
}
}
fn main() {
for n in [1, 6, 9] {
let day = Weekday::from_number(n);
println!("{n} -> {:?}, weekend: {}", day, day.is_weekend());
}
}1 -> Mon, weekend: false
6 -> Sat, weekend: true
9 -> Invalid, weekend: falsematches!(value, pattern) returns true if the value matches the pattern. It is a compact way to ask "is it one of these variants?".
8. Variants that carry data
Each variant can hold values. A tuple-like variant lists its data in parentheses.
#[derive(Debug)]
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle(f64, f64, f64),
}
fn main() {
let shapes = vec![
Shape::Circle(1.5),
Shape::Rectangle(3.0, 4.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for s in &shapes {
println!("{:?}", s);
}
}Circle(1.5)
Rectangle(3.0, 4.0)
Triangle(3.0, 4.0, 5.0)A Vec<Shape> holds circles, rectangles, and triangles together.
9. Getting the data out with match
Patterns in a match arm name the data inside a variant, and those names are available in the arm.
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0; // Heron's formula
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
}
fn main() {
let shapes = [Shape::Circle(1.0), Shape::Rectangle(2.0, 3.5), Shape::Triangle(3.0, 4.0, 5.0)];
let mut total = 0.0;
for s in &shapes {
let a = s.area();
total += a;
println!("{a:.3}");
}
println!("total area {total:.3}");
}3.142
7.000
6.000
total area 16.14210. Variants with named fields
A variant can also have named fields, like a struct.
#[derive(Debug)]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: char, shift: bool },
Resize { width: u32, height: u32 },
Quit,
}
fn handle(e: &Event) -> String {
match e {
Event::Click { x, y } => format!("click at ({x}, {y})"),
Event::KeyPress { key, shift: true } => format!("key {}", key.to_ascii_uppercase()),
Event::KeyPress { key, shift: false } => format!("key {key}"),
Event::Resize { width, height } => format!("resize to {width}x{height}"),
Event::Quit => String::from("quit"),
}
}
fn main() {
let events = [
Event::Click { x: 10, y: 20 },
Event::KeyPress { key: 'a', shift: true },
Event::KeyPress { key: 'b', shift: false },
Event::Resize { width: 800, height: 600 },
Event::Quit,
];
for e in &events {
println!("{}", handle(e));
}
}click at (10, 20)
key A
key b
resize to 800x600
quitThe pattern Event::KeyPress { key, shift: true } matches only key presses with shift held, and names the key field.
11. Mixing all kinds of variants
One enum can combine unit variants, tuple variants, and struct-like variants.
#[derive(Debug)]
enum Command {
Ping,
Echo(String),
Move { x: i32, y: i32 },
SetColor(u8, u8, u8),
}
fn main() {
let commands = vec![
Command::Ping,
Command::Echo(String::from("hello")),
Command::Move { x: 3, y: -1 },
Command::SetColor(0, 128, 255),
];
for c in &commands {
match c {
Command::Ping => println!("pong"),
Command::Echo(text) => println!("{text}"),
Command::Move { x, y } => println!("moving by ({x}, {y})"),
Command::SetColor(r, g, b) => println!("color #{r:02x}{g:02x}{b:02x}"),
}
}
}pong
hello
moving by (3, -1)
color #0080ff12. Matching through a reference binds references
When you match on a &Enum, the names in the patterns become references to the data, so nothing is moved out of the enum. This is why handle(e: &Event) above could read key and x without taking ownership.
enum Message {
Text(String),
Image { url: String, width: u32 },
}
fn preview(m: &Message) -> String {
match m {
Message::Text(body) => {
// `body` is a &String here
let short: String = body.chars().take(10).collect();
format!("text: {short}...")
}
Message::Image { url, width } => format!("image {width}px from {url}"),
}
}
fn main() {
let inbox = vec![
Message::Text(String::from("Meeting moved to 3pm tomorrow")),
Message::Image { url: String::from("https://example.com/cat.png"), width: 640 },
];
for m in &inbox {
println!("{}", preview(m));
}
println!("inbox still has {} messages", inbox.len());
}text: Meeting mo...
image 640px from https://example.com/cat.png
inbox still has 2 messages13. Changing the data inside a variant
Matching on &mut self gives &mut references to the fields.
#[derive(Debug)]
enum Account {
Active { balance: i64 },
Frozen { balance: i64, reason: String },
}
impl Account {
fn apply_interest(&mut self, percent: i64) {
match self {
Account::Active { balance } => *balance += *balance * percent / 100,
Account::Frozen { .. } => {} // frozen accounts earn nothing
}
}
}
fn main() {
let mut accounts = vec![
Account::Active { balance: 1_000 },
Account::Frozen { balance: 5_000, reason: String::from("fraud review") },
];
for a in &mut accounts {
a.apply_interest(5);
}
println!("{:?}", accounts);
}[Active { balance: 1050 }, Frozen { balance: 5000, reason: "fraud review" }]{ .. } ignores all the fields of a variant.
14. Consuming an enum to change state
A method taking self can return a new variant, moving data from the old state into the new one without cloning.
#[derive(Debug)]
enum Connection {
Disconnected,
Connecting { host: String, attempt: u32 },
Connected { host: String, session_id: u64 },
}
impl Connection {
fn step(self) -> Connection {
match self {
Connection::Disconnected => Connection::Connecting { host: String::from("db.internal"), attempt: 1 },
Connection::Connecting { host, attempt } if attempt < 3 => {
Connection::Connecting { host, attempt: attempt + 1 }
}
Connection::Connecting { host, .. } => Connection::Connected { host, session_id: 0xBEEF },
connected => connected,
}
}
}
fn main() {
let mut conn = Connection::Disconnected;
for _ in 0..5 {
println!("{:?}", conn);
conn = conn.step();
}
}Disconnected
Connecting { host: "db.internal", attempt: 1 }
Connecting { host: "db.internal", attempt: 2 }
Connecting { host: "db.internal", attempt: 3 }
Connected { host: "db.internal", session_id: 48879 }if attempt < 3 is a match guard, from Control Flow. The last arm connected => connected matches anything else and returns it unchanged.
15. Enums holding structs
A common style is to define a struct for each variant's data, which lets you pass the data around on its own.
#[derive(Debug)]
struct CardPayment {
last4: String,
amount_cents: u64,
}
#[derive(Debug)]
struct BankTransfer {
iban: String,
amount_cents: u64,
}
#[derive(Debug)]
enum Payment {
Card(CardPayment),
Bank(BankTransfer),
Cash(u64),
}
fn fee_cents(p: &Payment) -> u64 {
match p {
Payment::Card(c) => 30 + c.amount_cents * 29 / 1000,
Payment::Bank(b) => if b.amount_cents > 100_000 { 0 } else { 50 },
Payment::Cash(_) => 0,
}
}
fn main() {
let payments = [
Payment::Card(CardPayment { last4: String::from("4242"), amount_cents: 10_000 }),
Payment::Bank(BankTransfer { iban: String::from("DE89 3704"), amount_cents: 250_000 }),
Payment::Cash(1_500),
];
for p in &payments {
println!("fee {:>3} for {:?}", fee_cents(p), p);
}
}fee 320 for Card(CardPayment { last4: "4242", amount_cents: 10000 })
fee 0 for Bank(BankTransfer { iban: "DE89 3704", amount_cents: 250000 })
fee 0 for Cash(1500)16. How big is an enum?
An enum is as large as its largest variant, plus a small tag saying which variant it is. There is no heap allocation unless a variant's data allocates.
enum Small {
A,
B,
C,
}
enum Mixed {
Flag(bool),
Number(u64),
Point(f64, f64),
}
fn main() {
println!("Small: {} byte", std::mem::size_of::<Small>());
println!("Mixed: {} bytes", std::mem::size_of::<Mixed>());
println!("Option<Box<i32>>: {} bytes", std::mem::size_of::<Option<Box<i32>>>());
let _ = (Small::A, Small::B, Small::C, Mixed::Flag(true), Mixed::Number(1), Mixed::Point(0.0, 0.0));
}Small: 1 byte
Mixed: 24 bytes
Option<Box<i32>>: 8 bytesMixed needs 16 bytes for its largest variant, Point(f64, f64), plus 8 bytes for the tag (rounded up for alignment). The last line shows a trick you will meet in Option: the compiler can sometimes hide the tag inside the data for free.
17. A recursive enum needs indirection
An enum that contains itself directly would be infinitely large, so the compiler rejects it:
enum Expr {
Number(f64),
Add(Expr, Expr),
}
fn main() {}error[E0072]: recursive type `Expr` has infinite size
--> src/main.rs:1:1
|
1 | enum Expr {
| ^^^^^^^^^
2 | Number(f64),
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),
| ++++ +Box<T> stores a value on the heap and holds just a pointer to it, which has a fixed size. This fixes the problem. Box covers Box in detail.
18. An expression tree with Box
enum Expr {
Number(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}
impl Expr {
fn eval(&self) -> f64 {
match self {
Expr::Number(n) => *n,
Expr::Add(a, b) => a.eval() + b.eval(),
Expr::Mul(a, b) => a.eval() * b.eval(),
Expr::Neg(e) => -e.eval(),
}
}
fn render(&self) -> String {
match self {
Expr::Number(n) => n.to_string(),
Expr::Add(a, b) => format!("({} + {})", a.render(), b.render()),
Expr::Mul(a, b) => format!("{} * {}", a.render(), b.render()),
Expr::Neg(e) => format!("-{}", e.render()),
}
}
}
fn main() {
// (2 + 3) * -4
let e = Expr::Mul(
Box::new(Expr::Add(Box::new(Expr::Number(2.0)), Box::new(Expr::Number(3.0)))),
Box::new(Expr::Neg(Box::new(Expr::Number(4.0)))),
);
println!("{} = {}", e.render(), e.eval());
}(2 + 3) * -4 = -2019. Vec already provides the indirection
A variant that holds a Vec of the same enum does not need Box, because a Vec stores its elements on the heap already. This is how a JSON value can be modeled.
enum Json {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Vec<Json>),
Object(Vec<(String, Json)>),
}
impl Json {
fn render(&self) -> String {
match self {
Json::Null => String::from("null"),
Json::Bool(b) => b.to_string(),
Json::Number(n) => n.to_string(),
Json::Str(s) => format!("\"{s}\""),
Json::Array(items) => {
let parts: Vec<String> = items.iter().map(|i| i.render()).collect();
format!("[{}]", parts.join(","))
}
Json::Object(fields) => {
let parts: Vec<String> = fields.iter().map(|(k, v)| format!("\"{k}\":{}", v.render())).collect();
format!("{{{}}}", parts.join(","))
}
}
}
}
fn main() {
let doc = Json::Object(vec![
(String::from("id"), Json::Number(7.0)),
(String::from("name"), Json::Str(String::from("Ferris"))),
(String::from("tags"), Json::Array(vec![Json::Str(String::from("rust")), Json::Bool(true)])),
(String::from("manager"), Json::Null),
]);
println!("{}", doc.render());
}{"id":7,"name":"Ferris","tags":["rust",true],"manager":null}items.iter().map(|i| i.render()).collect() renders every element into a Vec<String>. It is an iterator pipeline, the Rust equivalent of items.stream().map(...).toList(), covered in Iterators.
20. if let for one variant
When you care about only one variant, if let is shorter than a full match. Pattern Matching covers it in depth.
enum Notification {
Email { to: String, subject: String },
Sms { number: String },
Push,
}
fn main() {
let queue = vec![
Notification::Sms { number: String::from("+15550100") },
Notification::Email { to: String::from("ops@example.com"), subject: String::from("Disk at 91%") },
Notification::Push,
];
for n in &queue {
if let Notification::Email { to, subject } = n {
println!("email to {to}: {subject}");
}
}
let sms_count = queue.iter().filter(|n| matches!(n, Notification::Sms { .. })).count();
println!("{sms_count} SMS in the queue");
}email to ops@example.com: Disk at 91%
1 SMS in the queue21. Realistic example: parsing commands
Turning text into an enum at the edge of a program means the rest of the code works with well-typed values instead of strings.
#[derive(Debug)]
enum Command {
Get(String),
Set(String, String),
Delete(String),
List,
Unknown(String),
}
fn parse(line: &str) -> Command {
let parts: Vec<&str> = line.split_whitespace().collect();
match parts.as_slice() {
["GET", key] => Command::Get(key.to_string()),
["SET", key, value] => Command::Set(key.to_string(), value.to_string()),
["DEL", key] => Command::Delete(key.to_string()),
["LIST"] => Command::List,
_ => Command::Unknown(line.to_string()),
}
}
fn main() {
for line in ["SET color blue", "GET color", "DEL color", "LIST", "FLY away"] {
println!("{:?}", parse(line));
}
}Set("color", "blue")
Get("color")
Delete("color")
List
Unknown("FLY away")parts.as_slice() gives a slice, and each arm matches its exact shape, like the slice patterns in Slices.
22. Realistic example: a key-value store driven by commands
enum Command {
Set(String, String),
Get(String),
Delete(String),
}
struct Store {
entries: Vec<(String, String)>,
}
impl Store {
fn execute(&mut self, cmd: Command) -> String {
match cmd {
Command::Set(key, value) => {
for entry in &mut self.entries {
if entry.0 == key {
entry.1 = value;
return String::from("OK (updated)");
}
}
self.entries.push((key, value));
String::from("OK")
}
Command::Get(key) => {
for (k, v) in &self.entries {
if *k == key {
return v.clone();
}
}
String::from("(nil)")
}
Command::Delete(key) => {
let before = self.entries.len();
self.entries.retain(|(k, _)| *k != key);
format!("deleted {}", before - self.entries.len())
}
}
}
}
fn main() {
let mut store = Store { entries: Vec::new() };
let script = vec![
Command::Set(String::from("user:1"), String::from("ada")),
Command::Get(String::from("user:1")),
Command::Set(String::from("user:1"), String::from("grace")),
Command::Get(String::from("user:1")),
Command::Delete(String::from("user:1")),
Command::Get(String::from("user:1")),
];
for cmd in script {
println!("{}", store.execute(cmd));
}
}OK
ada
OK (updated)
grace
deleted 1
(nil)execute takes each Command by value, so the Strings inside move straight into the store without cloning. retain keeps only elements for which the closure returns true; see Closures.
23. Realistic example: event sourcing
Instead of storing an order's current state, store the events that happened to it, and compute the state by replaying them.
#[derive(Debug)]
enum OrderEvent {
Created { id: u32 },
ItemAdded { sku: String, qty: u32, price_cents: u64 },
ItemRemoved { sku: String },
Paid { amount_cents: u64 },
Shipped { carrier: String },
}
#[derive(Debug, Default)]
struct OrderState {
id: u32,
items: Vec<(String, u32, u64)>,
paid_cents: u64,
shipped_with: String,
}
fn replay(events: &[OrderEvent]) -> OrderState {
let mut state = OrderState::default();
for e in events {
match e {
OrderEvent::Created { id } => state.id = *id,
OrderEvent::ItemAdded { sku, qty, price_cents } => state.items.push((sku.clone(), *qty, *price_cents)),
OrderEvent::ItemRemoved { sku } => state.items.retain(|(s, _, _)| s != sku),
OrderEvent::Paid { amount_cents } => state.paid_cents += amount_cents,
OrderEvent::Shipped { carrier } => state.shipped_with = carrier.clone(),
}
}
state
}
fn main() {
let events = vec![
OrderEvent::Created { id: 12 },
OrderEvent::ItemAdded { sku: String::from("BOOK-1"), qty: 2, price_cents: 1_500 },
OrderEvent::ItemAdded { sku: String::from("PEN-9"), qty: 1, price_cents: 300 },
OrderEvent::ItemRemoved { sku: String::from("PEN-9") },
OrderEvent::Paid { amount_cents: 3_000 },
OrderEvent::Shipped { carrier: String::from("DHL") },
];
println!("{:#?}", replay(&events));
println!("state after 3 events: {:?}", replay(&events[..3]).items);
}OrderState {
id: 12,
items: [
(
"BOOK-1",
2,
1500,
),
],
paid_cents: 3000,
shipped_with: "DHL",
}
state after 3 events: [("BOOK-1", 2, 1500), ("PEN-9", 1, 300)]Because replay takes a slice, replaying only the first three events is just &events[..3].
24. Realistic example: a vending machine state machine
Each state is a variant, and each input produces the next state. The compiler checks that every state handles every input.
#[derive(Debug, Clone, Copy, PartialEq)]
enum State {
Idle,
HasMoney(u32),
Dispensing,
}
#[derive(Debug, Clone, Copy)]
enum Input {
InsertCoin(u32),
PressButton,
Refund,
Done,
}
const PRICE: u32 = 150;
fn transition(state: State, input: Input) -> (State, &'static str) {
match (state, input) {
(State::Idle, Input::InsertCoin(c)) => (State::HasMoney(c), "coin accepted"),
(State::HasMoney(m), Input::InsertCoin(c)) => (State::HasMoney(m + c), "coin accepted"),
(State::HasMoney(m), Input::PressButton) if m >= PRICE => (State::Dispensing, "dispensing"),
(State::HasMoney(_), Input::PressButton) => (state, "not enough money"),
(State::HasMoney(_), Input::Refund) => (State::Idle, "money returned"),
(State::Dispensing, Input::Done) => (State::Idle, "enjoy!"),
(_, _) => (state, "ignored"),
}
}
fn main() {
let inputs = [
Input::PressButton,
Input::InsertCoin(100),
Input::PressButton,
Input::InsertCoin(100),
Input::PressButton,
Input::InsertCoin(50),
Input::Done,
];
let mut state = State::Idle;
for input in inputs {
let (next, message) = transition(state, input);
println!("{:<14} + {:<15} -> {:<14} {message}", format!("{:?}", state), format!("{:?}", input), format!("{:?}", next));
state = next;
}
}Idle + PressButton -> Idle ignored
Idle + InsertCoin(100) -> HasMoney(100) coin accepted
HasMoney(100) + PressButton -> HasMoney(100) not enough money
HasMoney(100) + InsertCoin(100) -> HasMoney(200) coin accepted
HasMoney(200) + PressButton -> Dispensing dispensing
Dispensing + InsertCoin(50) -> Dispensing ignored
Dispensing + Done -> Idle enjoy!Matching on the tuple (state, input) checks both values at once. The final (_, _) arm handles every combination not listed above it.
25. Realistic example: a result type of your own
Before meeting the standard library's Result, it helps to see that it is just an enum. This validation returns either a value or a list of problems.
#[derive(Debug)]
enum Validated {
Valid { username: String, age: u8 },
Invalid(Vec<String>),
}
fn validate(username: &str, age: i32) -> Validated {
let mut problems = Vec::new();
if username.len() < 3 {
problems.push(format!("username '{username}' is too short"));
}
if !(13..=120).contains(&age) {
problems.push(format!("age {age} is out of range"));
}
if problems.is_empty() {
Validated::Valid { username: username.to_string(), age: age as u8 }
} else {
Validated::Invalid(problems)
}
}
fn main() {
for (name, age) in [("ferris", 8), ("al", 30), ("x", 200), ("grace", 45)] {
match validate(name, age) {
Validated::Valid { username, age } => println!("welcome {username} ({age})"),
Validated::Invalid(problems) => println!("rejected: {}", problems.join("; ")),
}
}
}rejected: age 8 is out of range
rejected: username 'al' is too short
rejected: username 'x' is too short; age 200 is out of range
welcome grace (45)26. Realistic example: pricing rules as data
Rules stored as enum values can be loaded from configuration, printed, and tested, instead of being hard-coded in if chains.
#[derive(Debug)]
enum Discount {
None,
Percent(u8),
FixedCents(u64),
BuyXGetY { buy: u32, free: u32 },
}
fn price_cents(unit_cents: u64, qty: u32, rule: &Discount) -> u64 {
let full = unit_cents * qty as u64;
match rule {
Discount::None => full,
Discount::Percent(p) => full - full * *p as u64 / 100,
Discount::FixedCents(off) => full.saturating_sub(*off),
Discount::BuyXGetY { buy, free } => {
let group = buy + free;
let free_items = qty / group * free;
unit_cents * (qty - free_items) as u64
}
}
}
fn main() {
let rules = [
Discount::None,
Discount::Percent(15),
Discount::FixedCents(2_000),
Discount::BuyXGetY { buy: 2, free: 1 },
];
for rule in &rules {
println!("{:<32} 7 x 4.00 = {:>6.2}", format!("{:?}", rule), price_cents(400, 7, rule) as f64 / 100.0);
}
}None 7 x 4.00 = 28.00
Percent(15) 7 x 4.00 = 23.80
FixedCents(2000) 7 x 4.00 = 8.00
BuyXGetY { buy: 2, free: 1 } 7 x 4.00 = 20.0027. Realistic example: a tiny instruction interpreter
A stack machine whose program is a list of enum values. This is how bytecode interpreters are structured.
#[derive(Debug, Clone, Copy)]
enum Op {
Push(i64),
Add,
Mul,
Dup,
Swap,
Print,
}
fn run(program: &[Op]) {
let mut stack: Vec<i64> = Vec::new();
for op in program {
match *op {
Op::Push(n) => stack.push(n),
Op::Add => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(a + b);
}
Op::Mul => {
let b = stack.pop().unwrap();
let a = stack.pop().unwrap();
stack.push(a * b);
}
Op::Dup => {
let top = *stack.last().unwrap();
stack.push(top);
}
Op::Swap => {
let n = stack.len();
stack.swap(n - 1, n - 2);
}
Op::Print => println!("{:?}", stack),
}
}
}
fn main() {
// computes (3 + 4) * (3 + 4) and 10 * 2
let program = [
Op::Push(3), Op::Push(4), Op::Add, Op::Dup, Op::Mul, Op::Print,
Op::Push(2), Op::Push(10), Op::Swap, Op::Print, Op::Mul, Op::Print,
];
run(&program);
}[49]
[49, 10, 2]
[49, 20]match *op dereferences to copy the Op out, which works because Op is Copy. The unwrap() calls would panic on a malformed program; Result & the ? Operator shows how to report such errors properly.
Summary
- An
enumdefines a type whose value is exactly one of several variants. - Variants can be plain names, tuple-like (
Circle(f64)), or struct-like (Move { x: i32, y: i32 }). matchextracts the data and must handle every variant. Adding a variant turns every incompletematchinto a compile error.- Enums have
implblocks, andderiveworks on them. - An enum is as large as its largest variant plus a tag. Recursive enums need a
Box(or aVec). - Enums model states, commands, events, messages, and results far more safely than strings or integer codes.
Next: Option: Replacing null, the most-used enum in Rust.
