Enums in Rust are a way to define a type by counting its possible variants. Unlike enums in languages like C, C++, or Java, Rust’s enums can also carry associated data of varying types, similar to algebraic data types in functional languages. An enum value can only be one of its defined variants at any given time.
Why is this technique useful?
- It helps you model data where a thing can be in one of several states.
- It helps the compiler catch errors by forcing you to handle all the variants (when using pattern matching).
- It often makes code cleaner compared to “tagging” with other means (e.g. using a struct with a kind field + optional values, etc.).
Here are various code examples different forms and uses of enums in Rust:
Simple, Field-less Enums (C-style Enums)
In their simplest form, Rust enums can define a set of named constants, much like C-style enums.
enum IpAddrKind {
V4,
V6,
}
Here:
IpAddrKind
is the enum name (the type).V4
andV6
are variants of that type.- A value of type
IpAddrKind
must be eitherIpAddrKind::V4
orIpAddrKind::V6
.
You can create instances by using the enum name followed by :: and the variant name:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
But four
is not a V6
, and vice versa. The enum ensures that.
Enums can also have explicitly assigned integer discriminants:
enum HttpStatus {
Ok = 200,
NotModified = 304,
NotFound = 404,
}
If not specified, Rust assigns numbers starting at 0. You can cast a C-style enum to an integer.
Enums with Associated Data
Variants of an enum can carry additional data. This is one of Rust’s powerful features.
Rust’s enums are significantly more powerful because their variants can hold data, which can be of varying types and amounts.
Tuple Variants
These variants hold data like tuples.
enum IpAddr {
V4(String),
V6(String),
}
Here:
IpAddr::V4(String)
is a variant that holds aString
.IpAddr::V6(String)
also holds aString
.
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
Even more flexible: different variants can have different types and even different numbers of fields. E.g.:
enum IpAddrDifferent {
V4(u8, u8, u8, u8),
V6(String),
}
V4
takes fouru8
values (for the four bytes of IPv4).V6
takes aString
.
You instantiate them like function calls:
let home_ip_string = IpAddr::V4(String::from("127.0.0.1"));
let home_ip_u8 = IpAddrDifferent::V4(127, 0, 0, 1);
Struct-Like Variants
These variants hold named fields, similar to structs.
Example: Message enum with various types of data in its variants:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Quit
has no data.Move
has named fieldsx
andy
.Write
has a single string.ChangeColor
has three integers.
You instantiate struct-like variants using struct expression syntax:
let msg_move = Message::Move { x: 10, y: 20 };
Generic Enums
Enums can be generic over one or more types, making them highly flexible.
Example: The standard library’s Option<T> enum:
enum Option<T> {
None,
Some(T),
}
Some(T)
means there’s a value.None
means absence.
Rust doesn’t have “null” in the sense of many languages. If you want something that might be absent, you use Option<T>
. The compiler forces you to handle both cases. This helps avoid many bugs related to null.
Example: The standard library’s Result<T, E> enum:
enum Result<T, E> {
Ok(T),
Err(E),
}
-
Ok(T)
Represents a successful outcome with a value of type T -
Err(E)
Represents an error outcome with an error value of type E
Example: A custom generic enum Repeat for controlling function repetition:
enum Repeat<T, U> {
Continue(T),
Result(U),
Done,
}
-
Continue(T)
Request calling function again with T value -
Result(U)
Request stopping and returning result U -
Done
Request stopping without result