Sajiron
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! π
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.
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.
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.
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.
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.
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.
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! π