- An enum is a single type that contains variants, which represent the possible values of the enum at any given time.
- By convention, the enum name and its variants’ names should follow
PascalCase. - Can access the variants using the
::notation and the variant name. ex. Day::Sunday
enum Day {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
}
// π‘ Day is the enum. Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday are its variants.
- An enum variant can have either,
- No data (a unit variant)
- Unnamed ordered data (a tuple variant)
- Named data/ fields (a struct variant)
enum FlashMessage { Success, // π‘ A unit variant (no data) Error(u8, String), // π‘ A tuple variant (one or more , separated data) Warning { field: String, message: String }, // π‘ A struct variant (one or more , separated name: value data) } // π‘ FlashMessage is the emnum, Success, Error, Warning are its variants.
π‘ In Rust, the term “instantiation” is used to describe the act of creating a concrete instance of a type (struct or enum).
π‘ In Rust, the term “field” is used to describe a named component in a C-like struct & struct-like enum variant, and the term “element” is used to describe an unnamed component in a tuple struct & tuple-like enum variant. The term “member” is used to describe both.
π― More complex examples can be found on Generics, Impls and Traits, Lifetimes and Modules sections.
Instantiation
#![allow(unused)] // π‘ skip unused warnings, as we don't read fields in the enums
#[derive(Debug)]
enum FlashMessage { // Definition
Success,
Error(u32, String),
Warning { field: String, message: String },
}
fn main() {
// 1. Instantiation with separate variable declaration and assignment
let x: FlashMessage; // Declaration with the data type
x = FlashMessage::Success;
println!("{x:?}"); // Success
// 2. Instantiation with a direct variable initialization
let a = FlashMessage::Success;
let b = FlashMessage::Error(401, "Unauthorized".to_string());
let c = FlashMessage::Warning { field: "email".to_string(), message: "This is required".to_string() };
println!("{a:?}"); // Success
println!("{b:?}"); // Error(401, "Unauthorized")
println!("{c:?}"); // Warning { field: "email", message: "This is required" }
}
// 3. Instantiation with a default variant
#![allow(unused)] // π‘ skip unused warnings, as we don't use the all variants of the enum
#[derive(Debug, Default)]
enum Hand {
Left,
#[default] // π‘Set Right as the default variant
Right,
}
fn main() {
let a = Hand::default(); // Instantiation with the default variant
println!("{a:?}"); // Right
}
In Rust, the #[derive()] attribute is used to automatically generate an implementation of certain traits for a custom data structure (struct and enum), instead of you writing them by hand. The std::fmt::Debug trait allows us to format a value with {:?} or {:#?} in println! and similar macros. The std::default::Default trait allows us to create a new instance of a type with the Type::default() method.
Pattern Matching
With match
#![allow(unused)] // π‘ skip unused warnings, as we don't use the all variants of the enum
enum Season {
Spring,
Summer,
Autumn,
Winter,
}
fn main() {
let a = Season::Winter;
let result = match a {
Season::Spring => "βοΈ",
Season::Summer => "π",
Season::Autumn => "π",
Season::Winter => "βοΈ",
};
println!("{result}"); // βοΈ
}
With if let, else if let, else
if let is useful when we only care about handling one (or few) specific patterns and donβt need to explicitly match every possible case.
#![allow(unused)] // π‘ skip unused warnings, as we don't use the all variants of the enum
enum Season {
Spring,
Summer,
Autumn,
Winter,
}
fn main() {
let a = Season::Winter;
let result = if let Season::Spring = a {
"βοΈ"
} else if let Season::Summer = a {
"π"
} else if let Season::Autumn = a {
"π"
} else if let Season::Winter = a {
"βοΈ"
} else {
unreachable!()
};
println!("{result}"); // βοΈ
}
Destructuring & Accessing Variants’ Members
In Rust, directly accessing an enum variant’s fields without any form of pattern matching is not possible. We need to use pattern matching to access the fields by using a match expression or if let expression.
With match
#![allow(unused)] // π‘ skip unused warnings, as we don't use the all variants of the enum
enum FlashMessage {
Success,
Error(u32, String),
Warning { field: String, message: String },
}
fn main() {
let a = FlashMessage::Error(401, "Unauthorized".to_string());
let result = match a {
FlashMessage::Success => "We'll get back to you.".to_string(),
FlashMessage::Error(_, msg) => msg, // π‘ Destructuring only the second element of the tuple variant.
FlashMessage::Warning { message, .. } => message, // π‘ Destructuring only the second field of the struct variant.
};
println!("{result}"); // Unauthorized
}
With if let, else if let, else
if let is useful when we only care about handling one (or few) specific patterns and donβt need to explicitly match every possible case.
#![allow(dead_code)] // π‘ Remove dead_code warnings, as we don't access the all elements of variants.
enum FlashMessage {
Success,
Error(u32, String),
Warning { field: String, message: String },
}
fn main() {
let a = FlashMessage::Error(401, "Unauthorized".to_string());
if let FlashMessage::Error(_, msg) = a {
println!("{msg}"); // Unauthorized
} else if let FlashMessage::Warning { message, .. } = a {
println!("{message}");
} else {
println!("We'll get back to you.");
}
}