Safety and Efficiency with Rust

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 (.).


Loading Exercise...

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.

Loading Exercise...

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 (::).


Loading Exercise...

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.


Loading Exercise...

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.


Loading Exercise...

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.


Loading Exercise...