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:
match
If we want to avoid moving the data, we can match on a reference:
if let
if 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.