Rust Enum Class
Enum classes in Rust are not as straightforward as in other programming languages, but they can still be used quite simply:
Example
#[derive(Debug)]
enum Book {
Papery, Electronic
}
fn main() {
let book = Book::Papery;
println!("{:?}", book);
}
Run result:
Papery
Books are categorized into paper books (Papery book) and electronic books (Electronic book).
If you are currently developing a library management system and need to describe the different attributes of these two types of books (paper books have a call number, electronic books only have a URL), you can add tuple attributes to the enum members:
enum Book {
Papery(u32),
Electronic(String),
}
let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));
If you want to name the attributes, you can use struct syntax:
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
Although you can name them this way, note that you cannot access the attributes bound to the enum class like you would access struct fields. The access method is within the match syntax.
Match Syntax
The purpose of enums is to categorize a type of thing, and the goal of categorization is to describe different situations. Based on this principle, enum classes are often eventually processed by branching structures (switch in many languages). Switch syntax is classic, but Rust does not support it, and many languages are moving away from switch due to the common issue of cascading execution problems from forgetting to add breaks. Languages like Java and C# prevent this through safety checks.
Rust uses the match statement to implement branching structures. First, let's see how to handle enum classes with match:
Example
fn main() {
enum Book {
Papery {index: u32},
Electronic {url: String},
}
let book = Book::Papery{index: 1001};
let ebook = Book::Electronic{url: String::from("url...")};
match book {
Book::Papery { index } => {
println!("Papery book {}", index);
},
Book::Electronic { url } => {
println!("E-book {}", url);
}
}
}
Run result:
Papery book 1001
The match block can also be treated as a function expression; it can have a return value:
match enum_instance {
Category1 => return_value_expression,
Category2 => return_value_expression,
...
}
But all return value expressions must be of the same type!
If the enum class attributes are defined as tuples, you need to specify a temporary name in the match block:
Example
enum Book {
Papery(u32),
Electronic {url: String},
}
let book = Book::Papery(1001);
match book {
Book::Papery(i) => {
println!("{}", i);
},
Book::Electronic { url } => {
println!("{}", url);
}
}
Match can not only branch on enum classes but also on integers, floating-point numbers, characters, and string slice references (&str). Although branching on floating-point numbers is legal, it is not recommended due to potential precision issues that could lead to incorrect branching.
When branching on non-enum types, it is important to handle exceptional cases, even if there is nothing to do in those cases. Exceptional cases are represented by an underscore _
:
Example
fn main() {
let t = "abc";
match t {
"abc" => println!("Yes"),
_ => {},
}
}
Option Enum Class
The Option enum class in Rust is used to handle the absence of a value, which is a common scenario in programming. It is defined as follows:
enum Option<T> {
Some(T),
None,
}
Here, T
is a generic type parameter. The Some
variant represents a value of type T
, and None
represents the absence of a value.
Example
fn main() {
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
match some_number {
Some(n) => println!("The number is {}", n),
None => println!("No number present"),
}
match some_string {
Some(s) => println!("The string is {}", s),
None => println!("No string present"),
}
match absent_number {
Some(n) => println!("The number is {}", n),
None => println!("No number present"),
}
}
Run result:
The number is 5
The string is a string
No number present
Using the Option enum helps to prevent common errors related to null values and ensures that the absence of a value is explicitly handled in the code. Option is an enum class in Rust's standard library, designed to fill the gap where Rust does not support null references.
Many languages support the existence of null (C/C++, Java), which is convenient but also creates significant problems. The inventor of null himself acknowledged this, stating, "A convenient idea has caused cumulative losses of $1 billion."
Null often delivers a fatal blow to programs when developers assume everything is not null: after all, the presence of just one such error can cause the program to terminate entirely.
To address this issue, many languages default to not allowing null but support its appearance at the language level (often indicated by the ? symbol before the type).
Java supports null by default but can restrict its occurrence with the @NotNull annotation, which is a workaround.
Rust completely disallows null values at the language level, but since null can efficiently solve a few problems, Rust introduced the Option enum class:
enum Option<T> {
Some(T),
None,
}
If you want to define a class that can be null, you can do so like this:
let opt = Option::Some("Hello");
If you want to perform operations on opt
, you must first check if it is Option::None
:
Example
fn main() {
let opt = Option::Some("Hello");
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
Running result:
Hello
If your variable starts as null, you should consider how the compiler knows what type it is when it is not null. Therefore, an Option that starts as null must specify the type explicitly:
Example
fn main() {
let opt: Option<&str> = Option::None;
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
Running result:
opt is nothing
This design makes null programming less straightforward, but it is essential for building a stable and efficient system. Since Option is automatically imported by the Rust compiler, you can omit Option::
and simply write None
or Some()
when using it.
Option is a special enum class that allows value-based branching:
Example
fn main() {
let t = Some(64);
match t {
Some(64) => println!("Yes"),
_ => println!("No"),
}
}
if let Syntax
Example
let i = 0;
match i {
0 => println!("zero"),
_ => {},
}
Running result when placed in the main function:
zero
This program aims to check if i
is the number 0 and print "zero" if it is.
Now, shorten this code using the if let
syntax:
let i = 0;
if let 0 = i {
println!("zero");
}
The if let
syntax format is as follows:
if let matched_value = source_variable {
statement_block
}
You can add an else
block to handle exceptional cases.
The if let
syntax can be considered a "syntactic sugar" for a match statement that distinguishes only between two cases.
It is also applicable to enum classes:
Example
fn main() {
enum Book {
Papery(u32),
Electronic(String)
}
let book = Book::Electronic(String::from("url"));
if let Book::Papery(index) = book {
println!("Papery {}", index);
} else {
println!("Not papery book");
}
}
println!("Not a paper book");
}
}