Using structs
- One situation where we might want to use enums is when we’re working with IP addresses
- We know that any IP address is one of two things: a v4 or v6 address, and it cannot be both at the same time
- Enum values can only be 1 of some # of variants, so it’s perfect here
- We define the enumeration and its “variants” (types/fields) as follows
enum IpAddrKind {
V4,
V6,
}
- This is now a custom data type that we can use elsewhere in our code
- The variants of an enum are namespaced under its identifier, so we can access/use them like
IpAddrKind::V4
orIpAddrKind::V6
- Let’s say that we wanted to store the addresses themselves along with the variants
- Rust provides us with a way to do this, also
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
- And a useful thing is that we can store these addresses as different data types within enums
- This isn’t possible with vanilla structs!
- Just as we’re able to define methods on structs, we can define them also on enums, using the same
impl {ENUM_NAME}
syntax- And then we call these functions with the dot operator
Expressing nullity in Rust
- The null operator is error-prone in implementation, so Rust decides to exclude it from the language altogether
- In its place, it defines an enum
Option
that encodes the concept of a value being present or absent
- In its place, it defines an enum
enum Option<T> {
Some(T),
None,
}
- Some examples of using
Option
values to hold different types
let some_string = Some("a string");
let absent_number: Option<i32> = None;
- Notice that when we used
None
, we had to tell Rust what type ofOption<T>
we have because type inference is effectively impossible (there’s nothing to base it on) - So how is this useful to us? The problem with the null operator is operations involving it often have non-deterministic results when using possibly null values in functions
- In Rust, we can’t use the
Option<T>
items within calculations with non-Options as it results in type mismatches - Thus the compiler makes sure that we handle the null case before using a value with type
Option
- this is done by converting an item of typeOption<T>
to one of typeT
explicitly before using it
- In Rust, we can’t use the
match
expressions
- How do we do this?
match
expressionsmatch
expressions in Rust are analogous to switch statements in languages like C, and allow us to return different output for different enum values- We call these handlers “arms”, and a manifestation of these expressions is shown below
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
- When we store inner values of data within enums, we can actually use these within
match
expressions also. Continuing with the previous example, we haveCoin::Quarter(state) => 25
and in the enum,Quarter(UsState)
whereUsState
is another enum - One very common construct in Rust is using
match
statements to perform operations on values of typeOption<T>
- see below
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
- Pattern here: match against an enum, bind a variable to the data inside, and execute code based on it
- Rust’s
match
clauses must be exhaustive. In other words, all cases must be considered or our compiler is mad at us! - However this can get tedious when we have a ton of possibilities (such as a numerical range), so we can define
_ => (),
as our last arm and this means that all enum values that aren’t explicitly mapped have no effect
Introducing if let
- But even this can get wordy when we only care about one case. Introducing
if let
- Let’s say that we want to count all non-quarter coins while also announcing the state of the quarters that we see. We can write the following with our
match
control flow:
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}
- But this seems a little too verbose since we’re only checking for one pattern
- Instead, we can use an
if let
flow, which uses semantics that we’re familiar with as programmers
let mut count = 0;
if let Coin::Quarter(state) == coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
- The main trade-off here is that match statements are always exhaustive (else our compiler won’t let us proceed), whereas
if let
leaves a lot of discretion to the programmer and is a little less safe
References
- Chapter 6 of The Rust Programming Language by Steve Nichols and Nicole Klabnick.