Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

7.6 Custom Types and Pattern Matching

Introduction: Building Custom Data Structures

Section titled “Introduction: Building Custom Data Structures”

In systems programming, the ability to define custom data types is fundamental for modelling real-world entities and organising complex information. Just as C/C++ provides struct and class for this purpose, Rust offers struct and enum as tools for creating custom, type-safe data structures. For C/C++ engineers, understanding Rust’s approach to these constructs is crucial, as they integrate seamlessly with Rust’s ownership and borrowing system, contributing to its memory safety. This section introduces Rust’s structs and enums, with a focus on practical examples and comparisons to familiar C/C++ concepts that we covered earlier in the module.

Structs in Rust are custom data types that allow you to group together related pieces of data. They are similar to structs in C and C++ in that they are composite types, but Rust’s structs come with additional features and integrate deeply with the language’s safety guarantees.

Rust provides three kinds of structs: classic structs (with named fields), tuple structs (named tuples), and unit structs (field-less). All are defined using the struct keyword.

Classic Structs (Named Fields)
These are the most common type of struct, analogous to C/C++ structs or classes with public members. Each field within the struct has a name and a type.

For example, in Rust:

// Define a classic struct named User
struct User {
active: bool,
username: String, // String is Rust's growable, owned string type
email: String,
sign_in_count: u64,
}

Has the equivalence in C/C++:

// C++ equivalent using a struct (or use a class with public members)
struct User {
bool active;
std::string username; // std::string is C++'s growable string type
std::string email;
unsigned long long sign_in_count;
};

In C++, struct members are public by default, while class members are private by default. In Rust, struct fields are private by default within their module unless explicitly marked pub.

Tuple Structs (Named Tuples)
Tuple structs are essentially named tuples. They are useful when you want to give a name to a tuple and make it a distinct type, but the individual fields don’t need their own names. Rust Example:

// Define a tuple struct named Color
struct Color(i32, i32, i32); // Represents RGB values
// Define another tuple struct named Point
struct Point3D(i32, i32, i32); // Represents 3D coordinates

C/C++ Comparison: These are somewhat similar to type defining a std::tuple or a simple struct with unnamed members, but Rust’s tuple structs create distinct types.

// C++ equivalent using std::tuple (C++11 onwards)
using Color = std::tuple<int, int, int>;
using Point3D = std::tuple<int, int, int>;
// Or a C-style struct with unnamed fields (less common/idiomatic)
struct Color_C { int r, g, b; }; // Still usually named fields

Unit Structs (Field-less)
Unit structs are structs that have no fields at all. They are useful in situations where you need to implement a trait (we will cover traits in the next section) on some type but don’t need to store any data within the type itself. They are often used as marker types, i.e., when you want to distinguish different states or capabilities at the type level without storing any actual data within the struct itself. For example, you could have a Document that can be Pending or Published. You could use a generic Document<S> where S is the marker type.

struct Pending;
struct Published;
struct Document<State> { … }

There isn’t a direct, idiomatic C++ equivalent. They might be loosely compared to empty classes or structs used as tags.

To use a struct, you create an instance of it by providing concrete values for its fields. The following is a full code example for a Rust struct User (note that each separate instance of User is highlighted in the following example for clarity):

// Define a classic struct named User -- from previous example
struct User {
active: bool,
username: String, // String is Rust's growable, owned string type
email: String,
sign_in_count: u64,
}
fn main() { // 3 different ways to create the struct
// Create an instance of the User struct
let user1 = User {
active: true,
username: String::from("derek123"),
email: String::from("derek@dcu.ie"),
sign_in_count: 1,
};
println!("User1 email: {}", user1.email);
// Field init shorthand (if variable name matches field name)
let email = String::from("joe@example.com");
let username = String::from("joe456");
let user2 = User {
email, // This is the field init shorthand: equivalent to email: email
username, // Equivalent to username: username
active: false,
sign_in_count: 5,
};
println!("User2 email: {}", user2.email);
// Struct update syntax (like C++ copy constructor with modifications)
let user3 = User {
email: String::from("charlie@example.com"),
..user1 // Copy fields from user1. user1 is moved if its fields are not Copy
};
println!("User3 email: {}", user3.email); // Error if user1 was moved
println!("User3 username: {}", user3.username);
}

Note that for user2, email and username are used as shorthand because the local variable names match the struct’s field names. The active and sign_in_count fields are initialized explicitly as their corresponding local variables (if they existed) do not match their field names, or simply because the values are literals.

The code gives the output:

User1 email: derek@dcu.ie
User2 email: joe@example.com
User3 email: charlie@example.com
User3 username: derek123

In Rust, the .. (double-dot) syntax within a struct literal allows you to copy or move the remaining fields from another struct instance to create a new one, without having to explicitly list every field. This syntax is a convenient shorthand for creating slightly modified copies of existing struct instances.

When using struct update syntax (..user1), if user1 contains fields that are not Copy types (like String), user1 will be moved, and you won’t be able to use it afterward. If all fields are Copy types, user1 will be copied instead of moved.

C/C++ Equivalent (Classic Struct):

User user1 = {true, "derek123", "derek@dcu.ie", 1}; // Aggregate initialisation
User user2; // Default construction
user2.active = false;
user2.username = "joe456";
user2.email = "joe@example.com";
user2.sign_in_count = 5;
// C++ copy constructor with modifications (requires custom constructor)
User user3 = user1; // Copy construct
user3.email = "charlie@example.com";

Accessing and Modifying Fields
Fields of a struct instance are accessed using dot notation (.). To modify a field, the struct instance itself must be declared as mutable (let mut).

Rust Example: Using the same User and Color structs from the previous examples:

struct User {
active: bool,
username: String, // String is Rust's growable, owned string type
email: String,
sign_in_count: u64,
}
struct Color(i32, i32, i32); // Represents RGB values
fn main() {
let mut user1 = User {
active: true,
username: String::from("derek123"),
email: String::from("derek@dcu.ie"),
sign_in_count: 1,
};
// Access fields
println!("User email: {}", user1.email); // Output: User email: derek@dcu.ie
// Modify a field (user1 must be mutable)
user1.email = String::from("new_derek@dcu.ie");
println!("New user email: {}", user1.email); // Output: New user email
// Accessing fields of a tuple struct
let my_color = Color(255, 128, 0);
println!("Red: {}, Green: {}, Blue: {}", my_color.0, my_color.1, my_color.2);
}

Gives the output:

User email: derek@dcu.ie
New user email: new_derek@dcu.ie
Red: 255, Green: 128, Blue: 0

C/C++ Equivalent:

User user1 = {true, "derek123", "derek@dcu.ie", 1};
std::cout << "User email: " << user1.email << std::endl;
user1.email = "new_derek@dcu.ie";
std::cout << "New user email: " << user1.email << std::endl;
Concept Match

Match Struct Types and Features

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Classic Struct
drag a definition here…
Tuple Struct
drag a definition here…
Unit Struct
drag a definition here…
..syntax
drag a definition here…
pub
drag a definition here…

Definition Pool

A struct with named fields, similar to a C++ class with public members.
A field-less struct primarily used as a marker type.
The keyword required to make a struct or its fields visible outside its module.
The struct update syntax used to copy remaining fields from another instance.
A named collection of unnamed fields, accessed by index (e.g., .0).
Code Cloze
Rust

Struct Shorthand and Update Syntax

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1struct User {
2 active: bool,
3 username: String,
4}
5
6fn main() {
7 let username = String::from("derek123");
8 let user1 = User {
9 active: true,
10 ·····, // Field init shorthand
11 };
12
13 let user2 = User {
14 active: false,
15 ·····user1 // Struct update syntax
16 };
17}

Available Snippets

copy
name
...
..
String
username

In Rust, functions associated with a struct (or enum or trait) are defined within an impl (implementation) block. These can be either associated functions (like static methods or constructors in C++) or methods (instance methods in C++).

Rust Example (Full Code)

// Define a Point struct
struct Point {
x: f64,
y: f64,
}
// Implement methods and associated functions for Point
impl Point {
// Associated function (like a static method or constructor)
// Often used as a constructor, by convention named new
fn new(x: f64, y: f64) -> Self { //Self is an alias for the type Point
Point { x, y }
}
// Associated function to create an origin point
fn origin() -> Self {
Point { x: 0.0, y: 0.0 }
}
// Method (takes an immutable reference to self)
// &self is shorthand for self: &Self
fn distance_from_origin(&self) -> f64 {
((self.x * self.x) + (self.y * self.y)).sqrt()
}
// Method (takes a mutable reference to self)
// &mut self is shorthand for self: &mut Self
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
// Method (take of self)
// self is shorthand for self: Self
fn consume_and_print(self) {
println!("Consuming Point at ({}, {})", self.x, self.y);
// self is moved here, so the original variable cannot be used afterwards
}
}
fn main() {
let p1 = Point::new(3.0, 4.0); // Call associated function
let p_origin = Point::origin();
println!("Distance from origin for p1: {}", p1.distance_from_origin());
// Call method with immutable borrow
let mut p2 = Point::new(1.0, 2.0);
p2.translate(5.0, -1.0); // Call method with mutable borrow
println!("Translated p2: ({}, {})", p2.x, p2.y); // Output: Translated p2: (6, 1)
let p3 = Point::new(10.0, 20.0);
p3.consume_and_print(); // Call method that takes ownership
// println!("p3: ({}, {})", p3.x, p3.y); // ERROR: use of moved value: p3
}

This code gives the output:

Distance from origin for p1: 5
Translated p2: (6, 1)
Consuming Point at (10, 20)

This Rust code program demonstrates the powerful language features related to structs that make such object-oriented-like patterns not only possible but safe and efficient. Rust employs the struct keyword to define custom data types, bundling related data into a single unit. Here, our Point struct groups two f64 values, x and y, representing coordinates. This provides clear data encapsulation.

The flexibility emerges from the impl block, which is Rust’s mechanism for implementing associated functions and methods on a type. Think of an impl block as where you define the behaviour that the struct possesses, much like we did when writing classes in C++. There are two types of functions:

  • Firstly, we see Associated Functions. These are functions defined within the impl block that do not take self as their first parameter. They are invoked directly on the type itself, much like static methods or constructors in C++.
  • Secondly, we have Methods. These are functions defined within an impl block that do take self as their first parameter. Rust’s strict ownership system dictates how self is passed, and this is where safety and control is managed.

This is very similar to how we wrote this code in C++.

  • Associated Functions (Point::new(), Point::origin()) are directly analogous to static methods in C++ classes.
  • Methods (&self): These are equivalent to const member functions in C++. They receive an immutable reference to the instance (this pointer in C++ is implicitly const Point_Cpp* const).
  • Methods (&mut self): These are equivalent to non-const member functions in C++. They receive a mutable reference to the instance (this pointer is implicitly Point_Cpp* const).
  • Methods (self): These methods take ownership of the instance, meaning the original variable cannot be used after the call. This is similar to passing an object by value in C++ where the object is copied, or explicitly moving it (std::move) if a move constructor is defined.
Concept Match

Methods and self

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

&self
drag a definition here…
&mut self
drag a definition here…
self
drag a definition here…
Associated
drag a definition here…
Self
drag a definition here…

Definition Pool

A type alias within an impl block that refers to the type being implemented.
Takes ownership of the instance, moving it into the method.
Functions in an impl block that don't take self; often used as constructors.
An immutable reference to the instance, similar to a const member function in C++.
A mutable reference to the instance, allowing field modifications.
Code Order
Rust

Order an impl Block

Enums (enumerations) in Rust are powerful custom data types that allow you to define a type by enumerating its possible variants. Unlike simple C/C++ enums, Rust’s enums can have data associated with each variant, making them highly versatile for representing complex, structured choices. This makes them akin to “tagged unions” or “sum types” in other languages.

Enums are defined using the enum keyword, followed by the enum’s name and its variants.

Rust Example (Simple Enum - Unit-like Variants):

// Define a simple enum for traffic light colors
enum TrafficLight {
Red,
Yellow,
Green,
}

This is directly analogous to enum class in C++ (scoped enums).

enum class TrafficLight_Cpp {
Red,
Yellow,
Green
};

A key feature of Rust enums is that each variant can optionally hold data. This data can be structured like a tuple or like a struct.

Tuple-like Variants
These variants hold unnamed data, similar to tuple structs.

// Enum for messages in a game, with tuple-like data
enum Message {
Quit, // No data
Move(i32, i32), // Holds x and y coordinates
Write(String), // Holds a String message
ChangeColor(i32, i32, i32), // Holds RGB values
}

Struct-like Variants
These variants hold named data, similar to classic structs.

// Enum for different shapes, with struct-like data
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { side1: f64, side2: f64, side3: f64 },
}

C++‘s std::variant (C++17 onwards) combined with std::visit provides similar functionality to Rust’s enums with associated data, acting as a type-safe tagged union. Before std::variant, a common pattern was to use a union combined with an enum discriminant, which was less type-safe.

Concept Match

Enum Variants and Data

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Simple Enum
drag a definition here…
Tuple-like
drag a definition here…
Struct-like
drag a definition here…
Tagged Union
drag a definition here…
std::variant
drag a definition here…

Definition Pool

A conceptual name for enums that can hold different types of data.
An enum variant that holds unnamed data fields.
A list of named variants with no associated data.
An enum variant that holds named data fields.
The modern C++ equivalent to Rust's enums with associated data.
Code Cloze
Rust

Defining Enums with Data

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1enum WebEvent {
2 PageLoad,
3 KeyPress(char),
4 Paste(·····),
5 Click { x: i32, y: i32 },
6}
7
8fn main() {
9 let pressed = WebEvent::KeyPress('x');
10 let pasted = WebEvent::Paste(·····("hello"));
11}

Available Snippets

&str
String
char
String::from
content
value

The match expression is Rust’s primary way to handle enums with associated data. It allows you to execute different code blocks based on the variant of the enum, and it can also destructure the data contained within the variants. A crucial aspect of match is its exhaustiveness: the compiler ensures that all possible variants are handled, preventing unhandled cases that can lead to typical bugs in C++ switch statements.

The full code example is as follows:

enum Message {
Quit, // No data
Move(i32, i32), // Holds x and y coordinates
Write(String), // Holds a String message
ChangeColor(i32, i32, i32), // Holds RGB values
}
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { side1: f64, side2: f64, side3: f64 },
}
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("Quitting the application.");
}
Message::Move(x, y) => { // Destructuring tuple-like variant
println!("Moving to coordinates ({}, {}).", x, y);
}
Message::Write(text) => { // Destructuring String variant
println!("Writing message: \"{}\".", text);
}
Message::ChangeColor(r, g, b) => { // Destructuring tuple-like variant
println!("Changing color to RGB({}, {}, {}).", r, g, b);
}
}
}
fn calculate_shape_area(shape: Shape) -> f64 {
match shape {
Shape::Circle { radius } => { // Destructuring struct-like variant
std::f64::consts::PI * radius * radius
}
Shape::Rectangle { width, height } => { // Destructuring struct-like variant
width * height
}
Shape::Triangle { side1, side2, side3 } => { // Destructuring struct-like variant
// Heron's formula for triangle area
let s = (side1 + side2 + side3) / 2.0;
(s * (s - side1) * (s - side2) * (s - side3)).sqrt()
}
}
}
fn main() {
process_message(Message::Move(10, 20));
process_message(Message::Write(String::from("Hello Rust!")));
process_message(Message::Quit);
let circle = Shape::Circle { radius: 5.0 };
println!("Circle area: {}", calculate_shape_area(circle));
let rect = Shape::Rectangle { width: 4.0, height: 6.0 };
println!("Rectangle area: {}", calculate_shape_area(rect));
}

This gives the output:

Moving to coordinates (10, 20).
Writing message: "Hello Rust!".
Quitting the application.
Circle area: 78.53981633974483
Rectangle area: 24

Rust’s structs and enums provide powerful and flexible ways to define custom data types, essential for organising complex programs in systems and edge computing. Structs allow for clear grouping of related data, while enums offer a robust mechanism for representing distinct choices, with the added benefit of carrying associated data.

For C/C++ engineers, the key takeaways are:

  • Rust’s structs are similar to C/C++ structs, emphasising data aggregation. The impl blocks provide a structured way to define associated functions (like static methods) and methods (instance methods), with explicit &self, &mut self, and self to manage borrowing and ownership.
  • Rust’s enums are far more powerful than traditional C/C++ enums, acting as type-safe tagged unions that can hold diverse data for each variant.
  • The match expression, especially when used with enums, is critical to Rust’s safety. Its compile-time exhaustiveness check prevents unhandled cases, a common source of bugs in C++ switch statements.

These custom types are fully integrated with Rust’s ownership and borrowing system, enabling developers to write expressive, memory-safe, and performant code, which is particularly beneficial for resource-constrained and critical applications in edge programming and computer architectures.

Rust’s match expression is not limited to enums and scalars — it also works directly on slices. This is particularly useful when processing incoming byte buffers, sensor packets, or command sequences, where the structure of the data depends on its length.

A slice pattern matches on the “shape” of the slice: how many elements it contains and, optionally, what those elements are. The compiler enforces exhaustiveness just as it does for enums, so you cannot silently ignore a length case.

fn describe(readings: &[f32]) {
match readings {
[] => println!("No data received."),
[single] => println!("Single reading: {single:.2}"),
[a, b] => println!("Two readings: {a:.2} and {b:.2}"),
[first, .., last] => println!("Range: {first:.2} to {last:.2} ({} readings)", readings.len()),
}
}
fn main() {
describe(&[]);
describe(&[21.3]);
describe(&[21.3, 22.1]);
describe(&[21.3, 22.1, 21.9, 22.4, 23.0]);
}

This code gives the output, where you can see how the slides can be treated in different ways depending on the contents:

No data received.
Single reading: 21.30
Two readings: 21.30 and 22.10
Range: 21.30 to 23.00 (5 readings)

The .. pattern inside a slice ignores any number of middle elements. You can also bind the remaining elements to a name using rest @ ..:

fn process_packet(packet: &[u8]) {
match packet {
[] => eprintln!("Empty packet, discarded."),
[cmd] => println!("Single-byte command: {cmd:#04x}"),
[0xAA, 0xBB, payload @ ..] => {
println!("Valid header. Payload ({} bytes): {:?}", payload.len(), payload);
}
_ => eprintln!("Unknown packet format."),
}
}
fn main() {
process_packet(&[]);
process_packet(&[0x01]);
process_packet(&[0xAA, 0xBB, 0x10, 0x20, 0x30]);
process_packet(&[0xFF, 0x01]);
}

This code gives the output:

Empty packet, discarded.
Single-byte command: 0x01
Valid header. Payload (3 bytes): [16, 32, 48]
Unknown packet format.

The [0xAA, 0xBB, payload @ ..] pattern matches only packets that begin with the two-byte header 0xAA 0xBB, and binds everything after the header to the name payload as a &[u8] slice. This is a clean, readable way to parse binary protocols without index arithmetic.

Here is a short example for you to test your knowledge of enumerations. There is a small simulation below and underneath that there is a solution. Try not to look at the solution until you have at least attempted the question.

Write a Mobile Robot example that demonstrates an enum for robot movement and pattern matching. The code should define a Command enum with variants for Move, TurnLeft, and TurnRight. The Move variant should include a f64 value representing the distance to move. The Robot struct should store the robot’s position and direction in radians. There should also be an execute_command method that uses match for pattern matching on the Command enum, allowing the robot to respond appropriately to each command.

Give some example code to demonstrate it moving: For example, with the origin on the bottom left of the screen, start at the origin facing right (0.0, 0.0, 0.0), move forward by 5.0, turn left, move forward by 3.0, turn right three times, and move forward by 2.0. Don’t plot the output — it is here for illustrative purposes only.

Rust pattern matching

Robot Movement

Watch a mobile robot execute a sequence of Move / TurnLeft / TurnRight commands — its position and heading update live on the grid.

Robot Path
StartRobot
-10123456-101234XY(0,0)(0.0, 0.0)
Commands
Start
x = 0.0, y = 0.0, direction = 0 rad (→ East)
  1. 1Command::Move(5.0)
  2. 2Command::TurnLeft
  3. 3Command::Move(3.0)
  4. 4Command::TurnRight
  5. 5Command::TurnRight
  6. 6Command::TurnRight
  7. 7Command::Move(2.0)
Speed
Robot statex = 0.0y = 0.0direction = 0° (→ East)
Move(d)
x += d·cos(dir)
y += d·sin(dir)
TurnLeft
dir += PI / 2 (+90°)
TurnRight
dir -= PI / 2 (−90°)
Show Solution

Here is one possible solution that demonstrates the use of enums

use std::f64::consts::FRAC_PI_2; // Represents PI / 2
/// Command enum defines the possible actions for the robot.
pub enum Command {
Move(f64), // Move forward by a given distance
TurnLeft,
TurnRight,
}
/// Robot struct stores the state of our mobile robot.
#[derive(Debug)] // This allows us to print the struct for debugging.
pub struct Robot {
x: f64,
y: f64,
direction: f64, // Direction in radians
}
impl Robot {
/// Creates a new robot at a given position and direction.
pub fn new(x: f64, y: f64, direction: f64) -> Self {
Robot { x, y, direction }
}
/// Executes a command using pattern matching to update the robot's state.
pub fn execute_command(&mut self, command: Command) {
match command {
// If the command is Move, extract the distance and update x and y.
Command::Move(distance) => {
self.x += distance * self.direction.cos();
self.y += distance * self.direction.sin();
}
// If the command is TurnLeft, add 90 degrees (PI/2 radians) to the direction.
Command::TurnLeft => {
self.direction += FRAC_PI_2;
}
// Command is TurnRight, subtract 90 degrees (PI/2 radians) from direction.
Command::TurnRight => {
self.direction -= FRAC_PI_2;
}
}
}
}
fn main() {
// 1. Initialize the robot at the origin (0,0) facing right (0 radians).
let mut robot = Robot::new(0.0, 0.0, 0.0);
println!("Starting position: {:?}", robot);
// 2. Define a sequence of commands for the robot to execute.
// This is a nice way to do it. Calling robot.execute_command() directly is fine.
let commands = vec![
Command::Move(5.0),
Command::TurnLeft,
Command::Move(3.0),
Command::TurnRight,
Command::TurnRight,
Command::TurnRight,
Command::Move(2.0),
];
// 3. Execute each command and print the robot's state.
for (i, command) in commands.into_iter().enumerate() {
robot.execute_command(command);
println!("After command {}: {:?}", i + 1, robot);
}
}

This code gives the following output:

Starting position: Robot { x: 0.0, y: 0.0, direction: 0.0 }
After command 1: Robot { x: 5.0, y: 0.0, direction: 0.0 }
After command 2: Robot { x: 5.0, y: 0.0, direction: 1.5707963267948966 }
After command 3: Robot { x: 5.0, y: 3.0, direction: 1.5707963267948966 }
After command 4: Robot { x: 5.0, y: 3.0, direction: 0.0 }
After command 5: Robot { x: 5.0, y: 3.0, direction: -1.5707963267948966 }
After command 6: Robot { x: 5.0, y: 3.0, direction: -3.141592653589793 }
After command 7: Robot { x: 3.0, y: 2.9999999999999996, direction: -3.141592653589793 }

Traits are Rust’s primary mechanism for defining shared behaviour across multiple types — the Rust equivalent of C++ abstract classes or Java interfaces, but more flexible. A trait defines a set of method signatures that any conforming type must implement. The compiler enforces this contract at compile time.

A trait is declared with the trait keyword. Any struct or enum can then satisfy the contract using an impl Trait for Type block:

// Define a trait for any type that can report a sensor reading
trait SensorRead {
fn read(&self) -> f32; // required — every implementor must provide this
fn unit(&self) -> &str; // required
// Default implementation — used automatically unless the type overrides it
fn display(&self) {
println!("{:.2} {}", self.read(), self.unit());
}
}
struct TemperatureSensor { value: f32 }
struct HumiditySensor { value: f32 }
impl SensorRead for TemperatureSensor {
fn read(&self) -> f32 { self.value }
fn unit(&self) -> &str { "°C" }
// display() uses the default — no override needed
}
impl SensorRead for HumiditySensor {
fn read(&self) -> f32 { self.value }
fn unit(&self) -> &str { "%" }
fn display(&self) { // override the default with a custom format
println!("Humidity: {:.1}{}", self.read(), self.unit());
}
}
fn main() {
let temp = TemperatureSensor { value: 23.7 };
let hum = HumiditySensor { value: 58.1 };
temp.display(); // uses the default implementation
hum.display(); // uses the override
}
23.70 °C
Humidity: 58.1%

The rules:

  • A trait defines method signatures — the contract each implementor must honour.
  • A trait can also supply default method bodies, which are inherited automatically unless overridden.
  • Methods with no default body must be implemented by every conforming type; the compiler enforces this.

Trait Bounds: Generic Functions with Constraints

Section titled “Trait Bounds: Generic Functions with Constraints”

Traits also act as constraints on generic functions. The T: SensorRead syntax is a trait bound — it says “accept any type T, as long as it implements SensorRead”:

fn log_reading<T: SensorRead>(sensor: &T) {
sensor.display();
}
fn main() {
let temp = TemperatureSensor { value: 23.7 };
let hum = HumiditySensor { value: 58.1 };
log_reading(&temp);
log_reading(&hum);
}

This is static dispatch (monomorphisation): the compiler generates a separate, fully optimised version of log_reading for each concrete type used. There is zero runtime overhead compared to a hand-written function for each sensor type. Trait bounds and generics are covered in depth in Chapter 8.

When you need a collection of different types that all share a trait — without knowing their concrete types at compile time — use a trait object: &dyn SensorRead or Box<dyn SensorRead>:

fn log_all(sensors: &[&dyn SensorRead]) {
for s in sensors {
s.display();
}
}
fn main() {
let temp = TemperatureSensor { value: 23.7 };
let hum = HumiditySensor { value: 58.1 };
log_all(&[&temp, &hum]);
}

The dyn keyword signals that method dispatch happens at runtime via a vtable — the same mechanism as C++ virtual functions, and with the same small overhead. Section 7.7 discusses when to choose static dispatch (the default) versus dynamic dispatch (explicit opt-in).

Concept Match

Match Trait Concepts

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

trait
drag a definition here…
impl Trait for Type
drag a definition here…
Default Method
drag a definition here…
Trait Bound
drag a definition here…
dyn Trait
drag a definition here…

Definition Pool

A trait object enabling runtime dispatch via a vtable, analogous to C++ virtual functions.
A constraint on a generic parameter (T: Trait) enabling static, zero-cost dispatch.
A method body supplied in the trait definition and inherited unless overridden.
The syntax for providing a concrete implementation of a trait for a specific type.
A keyword that declares a set of method signatures defining shared behaviour.
Code Cloze
Rust

Implementing a Trait

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1trait Greet {
2 fn hello(&self) -> String;
3}
4
5struct Robot { name: String }
6
7····· Greet ····· Robot {
8 fn hello(&self) -> String {
9 format!("Hello from {}", self.name)
10 }
11}
12
13fn main() {
14 let r = Robot { name: String::from("R2D2") };
15 println!("{}", r.·····());
16}

Available Snippets

use
hello
for
impl
extend
of
trait
Code Cloze
Rust

Exhaustive Pattern Matching

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1enum Status {
2 Ok,
3 Error(u32),
4}
5
6fn check(s: Status) {
7 match s {
8 Status::Ok => println!("All good"),
9 ····· => println!("Something went wrong"),
10 }
11}
Expected Output
Something went wrong

Available Snippets

default
Status::Error
Status::Error(_)
_
Error
Code Cloze
Rust

Method Call Syntax

Code Order
Rust

Build a Complex Enum Match