Oliver Jumpertz
hero image

Rust's Enums Explained

Share this post to:

In programming, an enumerated type is a type that consists of a set of values. This basically makes them a union. Many programming languages thus provide something like enums for developers to use. In many of these languages, enums are only a type alias over an integer or a string.

Rust takes enums a whole level further by providing developers with variadic types that can actually contain other types and be as different from each other as possible. They are even so important in Rust that they form an essential building block of many programs. Especially as Rust misses inheritance, enums are often the way to go when developers want to write logic only once and use it for many associated tasks without using techniques like composition.

Every Rust developer should have deep knowledge about enums, which this article will be about.

If you enjoy watching a short video about this topic more, I have published one to YouTube that you might enjoy watching:


Enum Basics

Enums in programming, also called enumerated types, are data types consisting of a set of named values, also called members or member types. An enum is basically a union that can take the shape of whatever the enum states. In your code, any member is usually associated with a variable that has the type of the actual enum, which makes them comparable.

If you think about colors, you can quickly identify the three base colors of color theory:

That’s a perfect example of an enum. If you can give any color (achieved by mixing the three base colors) a name, you can also model it as an enum’s value. In code, you can express this fact as follows:

enum Color {
Red,
Green,
Blue,
White,
Black,
}

The code above does not attempt to show how to work with enums, but it shows the basic declaration and how to express an enumerated type. This declaration alone is enough to give readers some idea about what they deal with, and it’s a way better alternative than using (staying with the current example) a string with a hex code or, even worse, only the name of the color. The two latter examples would definitely raise some questions as it doesn’t become immediately apparent what the programmer actually intended to do.

Many programming languages implement enums only as a more type-safe and understandable way to define constants. Under the hood of these languages, enum members are usually associated with either an integer or a string. Taking TypeScript as an example, the following code shows two equivalent examples of using enums to declare constant values.

enum Color {
Red = 1,
Green = 2,
Blue = 3,
}
const Color = {
Red: 1,
Green: 2,
Blue: 3,
};

If you use this enum now, you can mainly use it to compare two different values.

if (colorOne === colorTwo) {
doSomething();
} else {
doSomethingElse();
}

This is usually the primary use case for enums in many programming languages. Enums are nested within objects and can then be used to make distinctions between different instances of these objects.

Some languages also take enums a little further. Java, for example, implements enums as special cases of classes, and Rust takes this even further by making them fully variadic types.


Enums In Rust

Basic Usage

Enums in Rust are more advanced than many other enum implementations, but they also still allow for the same fundamental use cases.

You can define an enum that you want to use to simply compare values at any time, as follows (you only need to derive PartialEq for it):

#[derive(PartialEq)]
enum PostType {
Tutorial,
News,
Guide,
Random,
}

And you can get a reference to a specific member relatively straightforward as follows:

let post_type = PostType::Guide;

After that, you can easily compare this variable with another variable of the same type:

if post_type == other_post_type {
...
} else {
...
}

Often, this kind of use case is enough for basic logic within the software you write, but there is more to enums, as you will learn in a moment.

Advanced Usage

Going back to the original analogy of colors, you could now ask yourself how you would potentially model the different color models. RGB, for example, defines color by mixing red, green, and blue. RGBA represents colors like RGB but additionally adds an alpha channel for transparency. CMYK defines color by combining Cyan, Magenta, Yellow, and Key (Black).

You could try to create a struct that combines all the traits of the different color models and provides methods to handle all the differences internally, but that would be an exhausting project. A better way is to use Rust’s enums as variadic types, which allows enums to include data and even let member types differ as much as you need them to.

Modeling the different color models in Rust can work as follows:

#[derive(PartialEq)]
enum ColorModel {
RGB(u8, u8, u8),
RGBA(u8, u8, u8, u8),
// Just another way to define a member and to
// give the properties of that member actual names
CMYK { cyan: u8, magenta: u8, yellow: u8, key: u8 },
}

Each enum member can contain either unnamed or named properties. The only difference is that instantiation and destructuring work a little differently. With this enum, you can now model RGB, RGBA, and CMYK colors.

You only need to instantiate a new enum and can then easily compare two values like this:

let rgb_red = ColorModel::RGB(255, 0, 0);
let rgba_red = ColorModel::RGBA(255, 0, 0, 255);
// with named members, you need to use curly braces and the properties' names
let cmyk_black: ColorModel = ColorModel::CMYK{cyan: 0, magenta: 0, yellow: 0, key: 255};
rgb_red == rgba_red; // false

Or go a step further and use a match statement to do work based on which color and model you deal with:

match color {
ColorModel::RGB(red, green, blue) => do_something_with_rgb(red, green, blue),
ColorModel::RGBA(red, green, blue, alpha) => do_something_with_rgba(red, green, blue, alpha),
ColorModel::CMYK { cyan, magenta, yellow, key } => do_something_with_cmyk(cyan, magenta, yellow, key),
}

That’s not everything that an enum can do, though. You can also add an implementation to any enum to build common logic affecting all member types. In this case, any implementation must handle all possible members, which is why such code can sometimes become cluttered. It is not uncommon to see a lot of match statements in these kinds of methods.

Let’s say you want to add a method to get a hex color code from any color enum. In this case, you can implement the enum as follows:

#[derive(PartialEq, Clone)]
enum ColorModel {
RGB(u8, u8, u8),
RGBA(u8, u8, u8, u8),
CMYK{ cyan: u8, magenta: u8, yellow: u8, key: u8},
}
impl ColorModel {
pub fn to_hex(&self) -> String {
match self {
ColorModel::RGB(red, green, blue) => format!("#{:X}{:X}{:X}", red, green, blue),
_ => self.to_rgb().to_hex(),
}
}
fn to_rgb(&self) -> Self {
match self {
ColorModel::RGB(_, _, _) => self.clone(),
ColorModel::RGBA(red, green, blue, alpha) => {
let red: u8 = (1 - alpha ) * 255 + alpha * red;
let green: u8 = (1 - alpha ) * 255 + alpha * green;
let blue: u8 = (1 - alpha ) * 255 + alpha * blue;
ColorModel::RGB(red, green, blue)
},
ColorModel::CMYK { cyan, magenta, yellow, key } => {
let red = 255 * (1 - cyan) * (1 - key);
let green = 255 * (1 - magenta) * (1 - key);
let blue = 255 * (1 - yellow) * (1 - key);
ColorModel::RGB(red, green, blue)
},
}
}
}

This is only a sneak peek at the power of Rust’s enums, but it should give you a relatively good idea about what they are capable of and what you can use them for. Interestingly, enums also have some impact on the memory level, which is what you will learn about next.


Memory Basics

Rust’s enums do, of course, also have a representation in memory, and it’s pretty good to know how this is precisely handled, as it can have quite an impact on the memory profile of your programs.

Technically, each enum needs a so-called discriminant. That’s an integer that enables the compiler to determine which variant of an enum your code currently deals with. It also makes comparisons between different member types easier. If the discriminant doesn’t match, there is no need to waste more CPU cycles comparing further properties.

Each nested property inside your enum takes up additional memory, and the cross-product of all properties defines the amount of memory all variants take up. There is no magic that makes enum members with no nested types smaller. But it’s probably best to look at an example.

In the image below, you can see a simple enum with two members. Simple is an enum without nested properties, and Complex is a member with two nested properties containing unsigned 64-bit integers. In your computer’s memory, even the Simple type will need space for the discriminant and both properties, although it doesn’t use them at all.

An enum with no properties that still takes up all the space of the complex member of that enum

Subsequently, Complex will make use of that space and place its properties inside the additional memory slots it has available:

An enum with all properties that takes up all the space it needs to reserve for its properties

In some rarer circumstances, the compiler can even optimize the discriminant away. In these cases, the compiler can use invalid bit patterns, which are basically patterns of bits for certain types that could never occur, like anything other than 0 or 1 for a boolean.

If such an opportunity occurs, the compiler optimizes the discriminant away. Instead, it sets the bit patterns of specific properties so that it becomes obvious which type the memory layout actually refers to. To understand this technique better, you can take a look at the example of a clever enum below:

A simple variant of a clever enum that uses an invalid bit pattern (2), which cannot occur for a boolean, and which thus uniquely identifies the variant

The compiler has no discriminant to know that it deals with Simple, but it knows that a bool can only take a 0 or 1, and additionally, it also knows that there is only one other member type. That is enough information to uniquely identify this memory layout as the member type Simple.

On the other hand, the Clever member of that enum would look as follows:

A clever variant of the enum with a single boolean property, which eliminates the need for a discriminant

As you can see, there are sometimes opportunities like this one, but they are rare. There are, however, some cases where a developer can really make a difference by constructing an enum in a way that actually eliminates the need for a discriminant.

Usually, you need knowledge about the memory layout of enums in other areas, like lists or vectors with many Options, when a majority of cases where a value is None can have a real impact on your program’s memory consumption. The same is also true for Result, where a vast difference between the memory consumption of the Ok and Err case can have quite a negative effect.


Summary

Rust’s enums are more advanced than enums in other languages. Additionally, they also offer more flexibility. This is mainly because they are so fundamental for the language and the style of programming you have to apply when using it.

Enums are implemented as variadic types in Rust that can also contain data of any form and shape, and individual members can differ from each other as much as you need them to. This makes them very powerful and also interesting to use.

In memory, enums are represented as a cross-product of all properties of their respective member types, but sometimes the compiler adds a few optimizations that allow it to not add too many properties, and thus reduce the memory consumption of your program.


Share this post to: