Enums in Rust

Enums and structs are not Rust specific concepts. They are tools that many programming languages offer to define our own custom types while working with data.
Structs give us the power to combine multiple values in a single variable. Enums, on the other hand, allow us to define a particular set of values or type of values that a variable can hold. An enum is a type that can be one of multiple options, but, importantly:
It's guaranteed to be one of those options.
It's guaranteed to be only one of those options at the same time.
Sometimes we need to work with variables that may have more than one value. Rust allows us to encode these possible values as enums. Let’s work through a code example to understand this concept.
Example 1 - Booleans
You've almost certainly used a simple enum in other languages: booleans. A boolean is a type that can be either true or false. Rust doesn't actually bother defining bool as an enum, but it absolutely could, and it would look like this:
enum Bool {
True,
False
}
So if you've ever used a boolean and found it useful, that's an example of the sort of context where an enum is useful.
Example 2 - Chat Application
Let’s say you are building a chat application. Assume that you have a message struct defined as following:
struct Message {
// --- SNIP ---
type: // ???
}
In this example, I have snipped the unnecessary code that we don’t need and focused only on what is in our scope. In a real-life application this message struct will hold a lot more data.
Now, coming back to our problem statement, we want to define what type of message we are sending to the other user. Is it audio or video? However, we don’t want to create a separate variable for each of these types. Therefore, we use a single type variable for it. Here we can use an enum to solve our problem.
enum MessageType {
Video,
Audio,
}
struct Message {
// --- SNIP ---
type: MessageType,
}
fn main() {
// --- SNIP ---
let mut message = Message::new();
message.type = MessagType::Video;
}
Here, we define a MessagType enumeration and list all the possible types it can be. Note here that the variant of the enum is namespaced under it’s identifier and we use the :: to separate the two.
So what exactly is the type Video and Audio? To answer that, these are, in a way, our own custom types defined by us.
We can also optionally store values in the enum values. Let’s say we also want to add some media_attachment that can hold a url of an image or a video.
enum MediaAttachment {
Video(String),
Image(String),
}
struct Message {
// --- SNIP ---
type: MessageType,
media_attachment: ImageAttachment,
}
fn main() {
// --- SNIP ---
let mut message = Message::new();
message.type = MessagType::Video;
message.media_attachment = MediaAttachment::Video(String::from("https:/youtube.com/@bitstackdev"));
}
We attach url string to the Video variant of the enum directly, so there is no need for an extra struct. Here, it’s also easier to see another detail of how enums work: the name of each enum variant that we define also becomes a function that constructs an instance of that enum.
The advantage of enums over structs, in this context, is apparent with another use case. If we had defined multiple structs and used multiple variables to store video, audio or images and then we had to attach some common metadata to our MediaAttachment we will have to define it for each of those structs. In case of enums, it’s opposite. We can defined one single metadata handler function on our enum using impl. This reduces the verbosity of our code and helps us write cleaner code. However, we will have to perform pattern matching to attach specific metadata if our metadata depends on the type.
Enums Advantages Over NULL Values
Rust doesn’t have NULL value. This exclusion of NULL and replacing it with an extra-ordinary method of handling absent values is what makes Rust unique. This brings us to the Option enum.
enum Option<T> {
Some(T),
None,
}
The problem is not with NULL but the compiler implementation of it. Rust solves this in a very innovative way. Option is an enum that has two variants - Someand None. None indicates that there is no value. Some(T) can hold data of any type T. We need to annotate the type T otherwise the compiler will throw an error. So how is it different from using NULL. After all, we are using None to represent a NULL value.
Simply put, the compiler forces us to handle None and Some if we want to execute our Option<T> and T (where T can be any type) are different types, the compiler won’t let us use an Option<T> value as if it were definitely a valid value.
let x: i32 = 5;
let y: Option<i32> = Some(5);
let sum = x + y;
Here the compiler will throw an error:

This is because i32 and Option<i32> are different. i32 is definitive while Option<i32> is an enum which we will need to serialize the value to i32 if Some(i32) or handle it accordingly if None. The compiler will make sure that the value is always valid. Everywhere that a value has a type that isn’t an Option<T>, you can safely assume that the value isn’t NULL. In order to use the value T from Option<T>, you must handle all variants.
To handle multiple variants, we use match control flow. We shall discuss this in the next article.



