[The Rust Programming Language] 6. Enums and Pattern Matching
6. Enums and Pattern Matching
Enums allow you to define a type by enumerating its possible variants.
Rust’s enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.
6.1 Defining an Enum
Enum values can only be one of its variants.
1 | enum IpAddrKind { |
We’ve used a struct to bundle the kind and address values together, so now the variant is associated with the value.
Example:
1 | enum IpAddrKind { |
We can represent the same concept in a more concise way using just an enum, rather than an enum inside a struct, by putting data directly into each enum variant.
1 | enum IpAddr { |
We attach data to each variant of the enum directly, so there is no need for an extra struct.
There’s another advantage to using an enum rather than a struct: each variant can have different types and amounts of associated data.
1 | struct Ipv4Addr { |
This code illustrates that you can put any kind of data inside an enum variant: strings, numeric types, or structs, for example. You can even include another enum! Also, standard library types are often not much more complicated than what you might come up with.
There is one more similarity between enums and structs: just as we’re able to define methods on structs using impl
, we’re also able to define methods on enums. Here’s a method named call that we could define on our Message enum:
1 | impl Message { |
The body of the method would use self to get the value that we called the method on. In this example, we’ve created a variable m that has the value Message::Write(String::from(“hello”)), and that is what self will be in the body of the call method when m.call()
runs.
The Option Enum and Its Advantages Over Null Values
This section explores a case study of Option
, which is another enum defined by the standard library. The Option
type is used in many places because it encodes the very common scenario in which a value could be something or it could be nothing. Expressing this concept in terms of the type system means the compiler can check whether you’ve handled all the cases you should be handling; this functionality can prevent bugs that are extremely common in other programming languages.
Rust doesn’t have the null feature that many other languages have. Null is a value that means there is no value there. In languages with null, variables can always be in one of two states: null or not-null.
Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option<T>
, and it is defined by the standard library - https://doc.rust-lang.org/std/option/enum.Option.html as follows:
1 | enum Option<T> { |
Here are some examples of using Option values to hold number types and string types:
1 | let some_number = Some(5); |
You have to convert an Option<T>
to a T
before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn’t null when it actually is.
The match
expression is a control flow construct that does just this when used with enums: it will run different code depending on which variant of the enum it has, and that code can use the data inside the matching value.
6.2 The match Control Flow Operator
Rust has an extremely powerful control flow operator called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things; Chapter 18 covers all the different kinds of patterns and what they do. The power of match comes from the expressiveness of the patterns and the fact that the compiler confirms that all possible cases are handled.
1 | enum Coin { |
If you want to run multiple lines of code in a match arm, you can use curly brackets. For example, the following code would print “Lucky penny!” every time the method was called with a Coin::Penny but would still return the last value of the block, 1:
1 | fn value_in_cents(coin: Coin) -> u8 { |
Patterns that Bind to Values
In the match expression for this code, we add a variable called state to the pattern that matches values of the variant Coin::Quarter. When a Coin::Quarter matches, the state variable will bind to the value of that quarter’s state. Then we can use state in the code for that arm, like so:
1 | enum Coin { |
If we were to call value_in_cents(Coin::Quarter(UsState::Alaska)), coin would be Coin::Quarter(UsState::Alaska). When we compare that value with each of the match arms, none of them match until we reach Coin::Quarter(state). At that point, the binding for state will be the value UsState::Alaska. We can then use that binding in the println! expression, thus getting the inner state value out of the Coin enum variant for Quarter.
Matching with Option
We can also handle Option
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
Matches Are Exhaustive
Consider this version of our plus_one function that has a bug and won’t compile:
This code does not compile!
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
Rust knows that we didn’t cover every possible case and even knows which pattern we forgot! Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option
The _ Placeholder
The _
pattern will match any value. By putting it after our other arms, the _
will match all the possible cases that aren’t specified before it. The ()
is just the unit value, so nothing will happen in the _
case. As a result, we can say that we want to do nothing for all the possible values that we don’t list before the _
placeholder.
1 | let some_u8_value = 0u8; |
However, the match expression can be a bit wordy in a situation in which we care about only one of the cases. For this situation, Rust provides if let
.
More about patterns and matching can be found in chapter 18 - https://doc.rust-lang.org/book/ch18-00-patterns.html.
6.3 Concise Control Flow with if let
he if let
syntax lets you combine if
and let
into a less verbose way to handle values that match one pattern while ignoring the rest.
Using match
:
1 | let some_u8_value = Some(0u8); |
Using `if let’:
1 | let some_u8_value = Some(0u8); |
Using if let
means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces. Choosing between match and if let depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.
In other words, you can think of if let
as syntax sugar for a match
that runs code when the value matches one pattern and then ignores all other values.
1 | // let mut count = 0; |