Structs, Traits, Enums, and Generics
Learning Objectives
- You know how to define and use custom structs.
- You know how to define and use custom enums
- You know how to implement associated functions and methods for structs and enums.
Rust does not have classes, but it offers powerful alternatives: structs and enums. These, combined with traits and generics, allow organizing and managing data and behavior.
Structs and traits
Structs are custom data types that allow organizing related values together, while traits are used to define shared behavior between different types.
Defining and using structs
Structs are defined using the struct
keyword followed by the struct name and possible fields. The fields are defined with a name and a type.
struct FullName {
firstname: String,
lastname: String,
}
To use a struct, we create an instance by specifying the struct name and the values for the fields. The fields of a struct instance are accessed using the dot operator (.
).
Defining and using traits
Traits define shared behavior that types can implement, somewhat similar to interfaces in other languages. However, the behavior is not tied to a specific type, allowing for more flexibility and reusability.
Traits are defined with the trait
keyword followed by the trait name and the shared behavior as function signatures, where the first parameter is a reference to self
(i.e., the instance that implements the trait).
The behavior can include method signatures with or without default implementations. The following example defines a Greet
trait with two methods, where one of them has a default implementation.
trait Greet {
fn greet(&self) -> String;
fn loud_greet(&self) -> String {
format!("{}!", self.greet().to_uppercase())
}
}
To implement a trait for a type, we use the impl
keyword followed by the trait name, followed by the for
keyword, and the type name. The trait methods are then implemented for the type.
Below, we implement the Greet
trait for the FullName
struct. When the trait is implemented for the FullName struct, the methods defined in the trait can be called on instances of the struct FullName.
Common traits in Rust
Rust has several standard library traits. As an example, the Clone
trait is used to create a copy of a value, Copy
trait is used for types that can be copied by simply copying bits, and the Debug
trait is used for formatting a value for debugging purposes. Some of the common traits are derived using the #[derive(...)]
attribute, which automatically implements the trait for a struct.
In the following example, the Debug
trait is derived for the FullName
struct, allowing it to be printed using the {:?}
format specifier.
There are also traits like Display
that need to be implemented manually. The Display
trait is used for formatting a value for display purposes — similar to toString
in e.g. Dart.
Optional fields in structs
Structs can also have optional fields, which are defined using the Option
that we looked at earlier. The Option
type is an enum that represents either Some
value or None
. As an example, the following struct Contact
has a field name
of type FullName
and optional fields for email and phone number.
Recursive structs
Structs can also be recursive, meaning that a struct can contain a field that is of the same type as the struct itself. This can be useful for representing recursive data structures like linked lists or trees. However, when defining recursive structs, a Box
type is used to wrap the struct. This is because the size of the struct needs to be known at compile time, and a struct containing itself would be infinitely large.
In the following example, we define a Node
struct that contains an integer value and an optional next
field that points to another Node
.
Enums
An enum (short for enumeration) is a custom type that can represent one of several possible variants. Enums are particularly useful for modeling data that can take on different, but fixed, forms.
Defining and using enums
Enums in Rust can have C-like variants (without associated data), tuple-like variants (with unnamed associated data), or struct-like variants (with named associated data). This flexibility allows enums to represent a wide range of data structures.
#[derive(Debug)]
enum Switch {
On,
Off,
}
#[derive(Debug)]
enum Message {
Quit,
Move(i32, i32),
Write(String),
}
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
Creating an enum instance is done by specifying the enum name and the variant. The variant is separated from the enum name by a double colon (::
).
Working with enum variants
Enums are typically used with match
expressions and if let
constructs.
Methods on enums
Enums can have associated functions and methods, similar to structs.
Generics
Generics allow writing reusable code by abstracting over types. Generics are defined by placing type parameters in angle brackets (<>
) after the function, struct, enum, or trait name, where the type parameters can be used as placeholders for actual types.
Generic functions
As an example, the following function x
is generic over the type T
, which means it can be called with any type.
fn x<T>(value: T) {
// something
}
The type parameter can also be defined so that it accepts all types that implement a specific trait. This is done by specifying the trait bound after the type parameter with a colon (:
). As an example, the following print
function is generic over the type T
, which must implement the Debug
trait from Rust standard library.
The print
function can be called with any type that implements the Debug
trait, such as integers, strings, and floats.
Generic structs and enums
Generics can also be used with structs and enums. As an example, the following Pair
struct is generic over two types T
and U
.
struct Pair<T, U> {
first: T,
second: U,
}
Similarly, enums can also be generic. The following Result
enum is generic over two types T
and E
— as we know, there’s also one in Rust already.
enum Result<T, E> {
Ok(T),
Err(E),
}
The following showcases an Optional
enum, which is similar to the Option
type from the Rust standard library. The Optional
enum is generic over the type T
and has two variants: Some
with a value of type T
and None
.