Enumerations, or enums for short, are a set of predefined constants in a programming language. Each constant represents a specific value, and only one of these values can be active at any given time. A common use case of enumerations is to define a set of possible states, such as HTTP status codes.
enum HttpStatus {
Success,
Created,
Accepted,
NoContent,
// ...
}
In Rust, enums are a versatile data type that can be used for more than just grouping related constants. The Option type from the standard library, for example, is an enum that can either be None
or Some(T)
, where T is any type.
enum Option<T> {
None,
Some(T),
}
Enums, as previously mentioned, are useful for grouping similar types, like the example of HttpStatus
codes. Another common example is error codes, where enums can provide a clear and organized way to define a set of possible errors for a given context, for example, database errors.
enum DatabaseError {
PrivilegeNotGranted,
NoData,
ConnectionFailure,
// ...
}
In the example of DatabaseError
(1), the values have implicit discrimination. Rust will automatically assign numbers starting at 0 to each value in the enum.
assert_eq!(DatabaseError::PrivilegeNotGranted as i32, 0);
assert_eq!(DatabaseError::NoData as i32, 1);
assert_eq!(DatabaseError::ConnectionFailure as i32, 2);
We can use explicit discrimination to assign our values to the variants in an enum.
enum DatabaseError {
PrivilegeNotGranted = 1007,
NoData = 2000,
ConnectionFailure = 8008,
// ...
}
assert_eq!(DatabaseError::PrivilegeNotGranted as i32, 1007);
assert_eq!(DatabaseError::NoData as i32, 2000);
assert_eq!(DatabaseError::ConnectionFailure as i32, 8008);
Status and Errors are common examples of how enums can be used to group similar types, but let's explore a more complex and interesting use case.
In the video game Diablo, characters take damage as a result of fights, from a monster to a player or vice versa. A typical kind of enum for representing damage in the game could be:
enum Damage {
Physical,
Fire,
Cold,
Lightning,
Poison,
}
As previously mentioned, in Rust enums are much more and an enum can contain any kind of data. We can modify the types of damage to permit store ranges of damage.
enum Damage {
Physical { critical_hit: bool }, // 1
Fire(i32), // 2
Cold(i32, std::time::Duration), // 3
Lightning, // 4
Poison(i32, std::time::Duration), // 5
}
We've updated the Damage
enum.
The Physical
type (1) now includes an anonymous struct with a critical_hit boolean
property.
The Fire
type (2) now accepts an i32
integer type to represent the total damage dealt.
The Cold
type (3) includes two values, an i32
integer type for total damage and a std::time::Duration
value to represent the duration for which the character is slowed down.
The Lightning
type (4) does not include any value, as a character receiving a lightning
attack dies immediately.
The Poison
type (5) includes an i32
integer type for the total damage dealt and a std::time::Duration value
to represent the duration over which the character receives damage.
With the Damage
enum defined, we can now create instances of the Damage
type. Instantiating an enum involves explicitly using the enum's name followed by one of its variants, as we have seen in the assert_eq!
statement at the beginning of the post.
let physical = Damage::Physical {
critical_hit: false,
};
let fire = Damage::Fire(12);
let cold = Damage::Cold(4, std::time::Duration::from_secs(3));
let lightning = Damage::Lightning;
let poison = Damage::Poison(2, std::time::Duration::from_secs(6));
For a simple retrieval of an enum value, we can use if let
control flow operator.
let fire = Damage::Fire(12);
if let Damage::Fire(a) = fire {
println!("Received {} fire damage", a)
}
Using the keyword match
, we can use pattern matching to handle each variant.
fn take_damage(damage: Damage) {
match damage {
Damage::Physical { critical_hit } => {
if critical_hit {
println!("Received critical_hit damage. Character died")
} else {
println!("Received physical damage")
}
}
Damage::Fire(a) => println!("Received {} fire damage", a),
Damage::Cold(a, d) => {
println!("Received {} cold damage. Slowed for {:?} seconds", a, d)
}
Damage::Lightning => println!("Received lightning damage. Character died"),
Damage::Poison(a, d) => {
println!("Received {} poison damage over {:?} seconds", a, d)
}
}
}
Another characteristic of enums in Rust is that they can implement methods.
enum Damage {
// [...]
}
impl Damage {
fn parity_damage(&self, n: i32) -> Damage {
if n % 2 == 0 {
Damage::Fire(12)
} else {
Damage::Lightning
}
}
}
Enums in Rust, as seen with the Damage
enum example, are a powerful tool for grouping and organising related types. We can include any type of data or methods, making them extremely versatile.