Enums are a way of saying that a value is one of a possible set of values.
Enums can have multiple variants of the same type.
We can define enums that hold data.
Enums can hold data in their variants.
We avoid needing a struct:
fn main() {
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}Each variant can hold different types of data.
Variants are functions that constructs an instance of the enum.
| enums | structs | |
|---|---|---|
| type | multiple | same |
| method | yes | yes |
| functions | “variant” | associated |
We can add methods to enums:
Option EnumRust doesn’t have null.
Instead, it uses the Option<T> enum to represent a value that may or may not be present.
This is used when a value might be missing or absent.
Option Enummatch Control Flow ConstructThe match expression in Rust checks values against patterns, running code based on the match:
We can bind values inside match arms, which is how we extract values from enum variants:
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}When we compare that value with each of the match arms, none of them match until we reach Coin::Quarter(state)
Option<T>Handling the Option
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);If the Option contains a value (Some(i)), we perform an operation on it. If it’s None, we return None.
The arms’ patterns must cover all possibilities. Consider this version of our plus_one function:
We didn’t handle the None case, so this code will cause a bug.
_ PlaceholderThe _ pattern can be used for values that we don’t care about:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}Note that we have to put the catch-all (other) arm last because the patterns are evaluated in order
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}The _ matches any value but doesn’t bind it to a variable.
When matching on enums with non-copyable types like String, ownership is transferred carefully.
fn main() {
let opt: Option<String> = Some(String::from("Hello world"));
match opt {
Some(_) => println!("Some!"),
None => println!("None!")
};
println!("{:?}", opt);
}If we replace Some(_) with a variable name, like Some(s), then the program will NOT compile:
matchIf we want to avoid moving the data, we can match on a reference:
if letif let provides a more concise way to handle enum variants in a control flow:
From this:
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}To this:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}if let is less verbose than match but loses exhaustive checking.
match on specific patterns.Option enum is used to safely handle cases where a value might be present or absent.