Skip to content

Rust - Structure

While arrays and vectors are great for storing lists of the same type, most real-world data is more complex. A structure, or struct, is a custom data type that lets you group together and name multiple related values that make up a meaningful entity. These values, called fields, can be of different types.

Structs are the building blocks for modeling your application’s domain. For example, you can create a User struct to hold a username, email, and activity status all in one place.

You define a struct using the struct keyword, followed by its name and the fields with their types inside curly braces. To use a struct, you create an instance of it by specifying concrete values for each field.

// Add this attribute to allow printing the struct for debugging.
#[derive(Debug)]
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
// Create an instance of the User struct.
let user1 = User {
email: String::from("contact@example.com"),
username: String::from("exampleuser123"),
active: true,
sign_in_count: 1,
};
// Access fields using dot notation.
println!("Username: {}", user1.username);
// Print the entire struct using the debug formatter.
println!("User details: {:?}", user1);
}
Username: exampleuser123
User details: User { username: "exampleuser123", email: "contact@example.com", sign_in_count: 1, active: true }

To modify a struct’s fields, the entire instance must be marked as mutable with mut. Rust does not allow marking individual fields as mutable.

#[derive(Debug)]
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let mut user1 = User {
email: String::from("contact@example.com"),
username: String::from("exampleuser123"),
active: true,
sign_in_count: 1,
};
println!("Original email: {}", user1.email);
// Modify a field on the mutable instance.
user1.email = String::from("new.email@example.com");
println!("New email: {}", user1.email);
}
Original email: contact@example.com
New email: new.email@example.com

Understanding how structs interact with functions is key to understanding Rust’s ownership system. You can pass structs to functions by value (moving ownership) or by reference (borrowing).

If you pass a struct directly, ownership is moved to the function. The original variable can no longer be used.

struct Article { title: String, author: String }
// This function takes ownership of the Article.
fn process_article(article: Article) {
println!("Processing article: '{}' by {}", article.title, article.author);
}
fn main() {
let my_article = Article {
title: String::from("Understanding Ownership"),
author: String::from("Rust Coder")
};
process_article(my_article); // Ownership is moved here.
// The following line would cause a compile error because `my_article` has been moved.
// println!("Title: {}", my_article.title);
}

To allow a function to use a struct without taking ownership, pass a reference to it (&). This is almost always preferred for read-only access.

struct Article { title: String, author: String }
// This function borrows the Article immutably.
fn display_article(article: &Article) {
println!("Displaying article: '{}' by {}", article.title, article.author);
}
fn main() {
let my_article = Article {
title: String::from("Understanding Borrows"),
author: String::from("Rust Coder")
};
display_article(&my_article); // We pass a reference.
// This is fine! `my_article` was only borrowed, not moved.
println!("The article is still available: {}", my_article.title);
}

Methods are functions that are defined within the context of a struct (or an enum or trait). They are declared using an impl (implementation) block. The first parameter of a method is always self, which represents the instance of the struct the method is being called on.

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// Implementation block for the Rectangle struct.
impl Rectangle {
// This is a method. `&self` is a shorthand for `self: &Self`.
// It borrows the instance immutably.
fn area(&self) -> u32 {
self.width * self.height
}
// Another method to check if a rectangle can hold another.
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area() // Call the method using dot notation.
);
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}
The area of the rectangle is 1500 square pixels.
Can rect1 hold rect2? true

Functions within an impl block that do not take self as a parameter are called associated functions. They are associated with the struct type itself, not a specific instance. They are often used as constructors to create new instances of the struct. The idiomatic name for a constructor is new.

struct Point { x: f64, y: f64 }
impl Point {
// An associated function, often used as a constructor.
// It's called using the `::` syntax.
fn new(x: f64, y: f64) -> Point {
Point { x, y } // Field init shorthand syntax
}
// Another associated function.
fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
// A method to display the point.
fn display(&self) {
println!("Point(x: {}, y: {})", self.x, self.y);
}
}
fn main() {
// Call the `new` associated function.
let p1 = Point::new(10.5, 20.2);
p1.display();
// Call the `origin` associated function.
let origin = Point::origin();
origin.display();
}
Point(x: 10.5, y: 20.2)
Point(x: 0.0, y: 0.0)

Rust also offers two other struct variations for different use cases:

  • Tuple Structs: Useful when you want to give a tuple a name and make it a distinct type, but the fields don’t need names. struct Color(u8, u8, u8);
  • Unit-Like Structs: Structs with no fields at all. They are useful in generic programming or when you need to implement a trait on a type but don’t have any data to store. struct AlwaysEqual;