Many Rust beginners with a background in systems programming tend to use bool
(or even u8
— an 8-bit unsigned integer type) to represent "state".
For example, how about a bool
to indicate whether a user is active or not?
Initially, this might seem fine, but as your codebase grows,
you'll find that "active" is not a binary state. There are many
different states that a user can be in. For example, a user
might be suspended or deleted. However, extending the user struct
can get problematic, because other parts of the code might
rely on the fact that active
is a bool
.
Another problem is that bool
is not self-documenting. What does
active = false
mean? Is the user inactive? Or is the user
deleted? Or is the user suspended? We don't know!
Alternatively, you could use an unsigned integer to represent state:
This is slightly better, because we can now use different values to represent more states:
const ACTIVE: u8 = 0;
const INACTIVE: u8 = 1;
const SUSPENDED: u8 = 2;
const DELETED: u8 = 3;
let user = User ;
A common use-case for u8
is when you interface with C code.
In that case, using u8
might seemingly be the only option.
However, we could still wrap that u8
in a
newtype!
;
const ACTIVE: UserStatus = UserStatus;
const INACTIVE: UserStatus = UserStatus;
const SUSPENDED: UserStatus = UserStatus;
const DELETED: UserStatus = UserStatus;
let user = User ;
This way, we can still use u8
to represent state, but we can
now also put the type system to work (a common pattern in idiomatic Rust). For
example, we can define methods on UserStatus
:
And we can even define a constructor that validates the input:
It's still not ideal, however! Not even if you interface with C code, as we will see in a bit. But first, let's look at the recommended way to represent state in Rust.
Use Enums Instead!
Enums are a great way to model state inside your domain. They allow you to express your intent in a very concise way.
We can plug this enum into our User
struct:
But that's not all; in Rust, enums are much more powerful than in many other languages. For example, we can add data to our enum variants:
We can even represent state transitions:
use ;
Look how much ground we've covered with just a few lines of code! We can extend the application with confidence, knowing that we can't accidentally delete a user twice or re-activate a deleted user. Illegal state transitions are now impossible!
Using Enums to Interact with C Code
Earlier, I promised that you can still use enums, even if you have to interact with C code.
Suppose you have a C library with a user status type (I've omitted the other fields for brevity).
typedef struct User;
User *;
You can write a Rust enum to represent the status:
Noticed that #[repr(u8)]
attribute? It tells the compiler to represent this
enum as an unsigned 8-bit integer. This is critical for compatibility with the C
code.
Now, let's wrap the C function in a safe Rust wrapper:
extern "C"
The Rust code now communicates with the C code using a rich enum type, allowing for more expressive and type-safe code.
If you want, you can play around with the code on the Rust playground.
Conclusion
Enums in Rust are more powerful than in most other languages. They can be used to elegantly represent state transitions — even across language boundaries.
You should consider using enums whenever you need to represent a set of possible values, like when representing the state of an object.