Enumerations
In C and C++, enums are tied to a value:
enum Level {
Low, Medium, High
}
Level n = Low;
You can do the same thing in Rust:
#![allow(unused)] fn main() { enum Level { Low, Medium, High } let n = Level::Low; }
C and C++ let you assign specific values to enumerations and cast them into numeric types:
enum Level { Low=1, Medium=2, High=3 }
Level n = Medium;
int o = n;
Rust lets you do the same:
#![allow(unused)] fn main() { enum Level { Low = 1, Medium = 2, High = 3 } let n = Level::Medium; let o: u8 = n; }
Rust lets you specify the underlying type for numeric Enums:
#![allow(unused)] fn main() { #[repr(u8)] enum Level { Low = 1, Medium = 2, High = 3 } let n = Level::Medium; }
Enumerations Can Contain Data
Rust enumerations can also contain data. They are effectively a tagged union, or variant. They will be the size of the largest possible member. The match
command is the best way to access data within an enumeration.
#![allow(unused)] fn main() { enum Command { DoNothing, Count(i32), // A tuple-style enumeration entry Search{ term: String, max_depth: i32 }, // A structure-style enumeration entry } let c = Command:DoNothing; let c = Command::Count(12); let c = Command::Search { term: "term".to_string(), max_deptth: 12 }; match c { Command::DoNothing => {} Command::Count(n) => { for i in 0..n { println!("{n}") } } Command::Search{ term, max_depth } => {} } }
It's important to remember that an enumeration only contains data for the assigned value---it is equal to exactly one option.
Remember we've used Option
and Result
? They are enumerations, using generics.
Option
Rust doesn't have null
values (it does in FFI code, but not in "safe" Rust). Whenever a value may or may not be present, it is wrapped in an Option
. Options are generic (we'll talk about that in the generics section)---it can contain pretty much anything. The declaration for Option
looks like this:
#![allow(unused)] fn main() { enum Option<T> { None, Some(T) } }
You've seen unwrap()
(an associated function that returns the option's content or panics). You can also access an Option with a match
statement:
#![allow(unused)] fn main() { match my_option { Some(x) => // Do something with x None => {} } }
The "if let" statement is a one-option match
. You can use it to destructure an Option. It's often preferable, because there are only two possibilities:
#![allow(unused)] fn main() { if let Some(x) = my_option { // You can use x here } else { // If you want to do something else } }
if let
is conditional upon destructuring succeeding. Solet x = Some(x)
is pattern matching. If it succeeded, thenif let
fires. You can also usewhile let
to perform the comparison on an iterator.
Result
Result
is also an enumeration, indicating a fallible action. It's very similar to the new std::expected
in C++. Rust's Results are:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T) Err(E) } }
They are generic (just like options), effectively templated in C++ parlance. When an operation may fail, it returns either Ok(good_value)
or Err(error type)
. We'll talk a lot more about this in Error Handling.
Enumerations can have Associated Functions, too
You can use impl
blocks with enumerations, too.
enum MyEnum { A, B } impl MyEnum { fn new() -> Self { MyEnum::A } // A constructor fn print_me(&self) { match self { MyEnum::A => println!("The first option!"), MyEnum::B => println!("The second option!"), } } } fn main() { let e = MyEnum::new(); e.print_me(); }