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::V4orIpAddrKind::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
Optionthat 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
Optionvalues 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 typeTexplicitly before using it
- In Rust, we can’t use the
match expressions
- How do we do this?
matchexpressionsmatchexpressions 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
matchexpressions also. Continuing with the previous example, we haveCoin::Quarter(state) => 25and in the enum,Quarter(UsState)whereUsStateis another enum - One very common construct in Rust is using
matchstatements 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
matchclauses 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
matchcontrol 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 letflow, 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 letleaves 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.