S

Sajiron

24 min readPublished on Feb 20, 2025

Deep Dive into Rust Structs: A Comprehensive Guide

DALLΒ·E 2025-02-20 22.34.53 - A futuristic workspace with multiple monitors displaying Rust programming code, a mechanical keyboard, and a coffee mug with the Rust logo. The setup .webp

1. Introduction

Related Post
πŸ“– If you haven't read it yet, check out the previous blog: Ownership & Borrowing in Rust (Key Rust Feature)

After learning about Ownership & Borrowing, the next step in mastering Rust is understanding Structs. Structs are the foundation of structured data representation, making Rust code more readable, maintainable, and reusable.

In this guide, we will explore:

βœ… How structs help group related data
βœ… Different types of structs in Rust
βœ… Advanced struct features, including associated functions and generics

By the end of this blog, you’ll be able to write structured, expressive, and safe Rust code. Let’s dive in! πŸš€

2. Understanding Structs in Rust

A struct in Rust is a custom data type that groups related values together. Structs are commonly used to model real-world entities and help provide clarity when working with structured data.

2.1 What is a Struct?

A struct (short for structure) is a way to define a type that groups multiple related values together. This makes your data more structured and readable compared to using separate variables or tuples.

In Rust, there are three main types of structs:

Regular Structs: These contain named fields.

Tuple Structs: Similar to tuples but have a distinct type.

Unit-Like Structs: Structs with no fields, mainly used for markers.

2.2 Defining and Using Structs

Structs in Rust allow you to define related fields under a single type, making data more readable and organized.

Regular Structs (Named Fields)

Regular structs contain named fields, making them easy to understand and use.

struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

fn main() {
let user = User {
active: true,
username: String::from("rust"),
email: String::from("rust@infinitecircuits.dev"),
sign_in_count: 1,
};

println!("User: {} ({})", user.username, user.sign_in_count);
}

πŸ“Œ Key Takeaways:

The User struct serves as a blueprint for user-related data, grouping multiple fields into a single logical unit.

Fields like username and email use owned String types instead of string slices (&str) to ensure memory ownership.

The . notation is used to access individual struct fields, making it easy to retrieve and modify data.

Structs provide better clarity than tuples by allowing named fields instead of relying on positional access.

Tuple Structs

Tuple structs behave like regular tuples but define a distinct type.

struct Color(i32, i32, i32);
struct Point(f64, f64);

fn main() {
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0);

println!("Red color: ({}, {}, {})", red.0, red.1, red.2);
println!("Origin: ({}, {})", origin.0, origin.1);
}

πŸ“Œ Key Takeaways:

Useful for simple, related data without field names.

Still provides type safety (e.g., Color and Point are distinct types).

Less readable than named structs when accessing fields.

Unit-Like Structs

Unit-like structs contain no fields and are often used as markers or empty types.

struct AlwaysEqual;

impl AlwaysEqual {
fn check(&self) {
println!("This struct is always equal!");
}
}

fn main() {
let _marker = AlwaysEqual;
_marker.check();
}

πŸ“Œ Key Takeaways:

Used for marker traits or empty struct behavior.

Can implement methods even if they don’t store any data.

Useful in APIs where types need to exist but don’t carry information.

2.3 Using impl to Define Methods (Struct Constructors)

Rust allows you to define methods for structs using impl. The impl keyword is short for "implementation block" and is used to define functions associated with a struct. While Rust does not have predefined constructors like other languages (e.g., __init__ in Python or constructors in C++/Java), it is a common convention to define a new function inside impl to initialize structs.

Example: Creating a Struct with new Constructor

struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

impl User {
fn new(username: String, email: String) -> Self {
User {
active: true, // Default value
username,
email,
sign_in_count: 1, // Default value
}
}
}

fn main() {
let user = User::new(String::from("rust"), String::from("rust@infinitecircuits.dev"));
println!("User: {} ({})", user.username, user.sign_in_count);
}

πŸ“Œ Key Takeaways:

new is not a built-in function; it's a convention for defining a constructor.

It helps avoid repetition when setting default values.

Using Self in fn new(...) -> Self is an alias for the struct name inside the impl block.

Calling User::new(...) creates a struct instance with predefined defaults.

2.4 When to Use Direct Instantiation vs. new?

In Rust, struct instances can be created directly or through a custom new function inside an impl block. The choice between the two depends on your specific needs.

βœ… Use Direct Instantiation When:

You need full control over each field value.

There are no default values required for struct fields.

The struct instance varies each time it's created.

Required purely for data storage without behavior.

βœ… Use a new Function When:

Some fields have default values (e.g., active: true, sign_in_count: 1).

Struct creation follows a common pattern across multiple instances.

You want to simplify struct instantiation and improve code reusability.

Defining methods like new, allowing the struct to have behavior beyond just storing data.

2.5 More Struct Usecases

Generic Structs

Rust allows you to define structs with generics, making them more flexible.

struct Point<T> {
x: T,
y: T,
}

fn main() {
let integer_point = Point { x: 10, y: 20 };
let float_point = Point { x: 1.5, y: 3.7 };

println!("integer_point: x = {}, y = {}", integer_point.x, integer_point.y);
println!("float_point: x = {}, y = {}", float_point.x, float_point.y);
}

πŸ“Œ Key Takeaways:

Generics enable struct fields to hold multiple types.

This increases code reusability and type safety.

Structs with Associated Functions

Structs in Rust support associated functions, which are methods that are tied to the struct but do not require an instance of the struct to be called. This allows for structured and reusable logic for creating and interacting with structs.

struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
// Associated function to create a new Rectangle instance
fn new(width: u32, height: u32) -> Self {
Self { width, height }
}

// Method to calculate the area of the rectangle
fn area(&self) -> u32 {
self.width * self.height
}

// Method to check if a rectangle is a square
fn is_square(&self) -> bool {
self.width == self.height
}
}

fn main() {
let rect = Rectangle::new(10, 5);
println!("Area: {}", rect.area());
println!("Is square: {}", rect.is_square());
}

πŸ“Œ Key Takeaways:

impl allows defining methods associated with a struct.

The Self keyword refers to the struct type inside impl.

Associated functions like new create instances of the struct, enforcing structured initialization.

Methods like area and is_square operate on instances of the struct, providing useful behaviors.

The Rectangle::new() syntax calls an associated function without requiring an instance, while rect.area() and rect.is_square() require an instance.

Structs with Traits (Polymorphism-like Behavior)

Rust does not support traditional inheritance, but you can use traits to define shared behavior.

trait Describe {
fn describe(&self) -> String;
}

struct Animal {
name: String,
species: String,
}

impl Describe for Animal {
fn describe(&self) -> String {
format!("{} is a {}", self.name, self.species)
}
}

fn main() {
let cat = Animal { name: String::from("Whiskers"), species: String::from("Cat") };
println!("{}", cat.describe());
}

πŸ“Œ Key Takeaways:

Traits define common behavior that multiple structs can implement.

This enables polymorphism-like functionality in Rust.

impl TraitName for Struct binds a struct to a trait implementation.

Structs with Lifetimes (Handling References Safely)

Rust requires lifetimes to ensure references in structs are valid.

struct Book<'a> {
title: &'a str,
}

fn main() {
let title = String::from("Rust Programming");
let book = Book { title: &title };
println!("Book title: {}", book.title);
}

πŸ“Œ Key Takeaways:

Lifetimes ('a) prevent dangling references in structs.

Rust enforces strict memory safety with lifetime annotations.

Needed when struct fields store references instead of owned values.

Structs with Default Values Using #[derive(Default)]

Rust allows you to automatically generate a default instance of a struct using #[derive(Default)].

#[derive(Default)]
struct Config {
debug: bool,
max_connections: u32,
}

fn main() {
let default_config = Config::default();
println!("Debug mode: {}, Max connections: {}", default_config.debug, default_config.max_connections);
}

πŸ“Œ Key Takeaways:

#[derive(Default)] provides an easy way to generate default struct values.

You can override specific fields while using default values for others.

Mutable Structs and Field Updates

Rust allows mutating struct fields if the instance is declared mutable.

struct Counter {
count: u32,
}

fn main() {
let mut counter = Counter { count: 0 };
counter.count += 1;
println!("Counter: {}", counter.count);
}

πŸ“Œ Key Takeaways:

Use mut to modify struct fields.

Rust enforces ownership rules even when modifying struct fields.

Updating Structs Using Struct Update Syntax

Rust allows updating an instance of a struct by copying values from another instance.

struct User {
username: String,
email: String,
active: bool,
}

fn main() {
let user1 = User {
username: String::from("rust"),
email: String::from("rust@infinitecircuits.dev"),
active: true,
};

let user2 = User {
email: String::from("new_email@infinitecircuits.dev"),
..user1
};

println!("User2: {} - {}", user2.username, user2.email);
}

πŸ“Œ Key Takeaways:

The .. syntax copies remaining fields from another instance.

Fields with owned types (String) cannot be reused unless explicitly cloned.

3. Next Steps

Now that you’ve learned about Structs, the next step is understanding Traits, which allow for shared behavior and modular design in Rust.

Stay tuned for the next blog on Traits in Rust! πŸš€

πŸ’‘ If you found this helpful, please remember to leave a like! πŸ‘