Understanding Structs and Methods in Rust
Structs are Rust’s primary way to group related values into a custom data type. They make data models explicit and can be paired with impl blocks to define methods and associated functions that belong with that data.
What Is a Struct?
A struct is a custom type that combines related fields into one logical value. Rust structs are often used for domain models, configuration, state, request data, and many other structured values.
Types of Structs in Rust
Rust has three common struct forms:
- Named-field structs: fields have descriptive names.
- Tuple structs: fields are positional, like a tuple with its own type name.
- Unit structs: contain no fields and are useful as marker types or trait implementations.
Named-Field Structs
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("rustacean"),
email: String::from("rust@example.com"),
age: 30,
active: true,
};
println!(
"Username: {}, Email: {}, Age: {}, Active: {}",
user1.username, user1.email, user1.age, user1.active
);
}User defines four named fields. Instances are created with User { ... }, and fields are accessed with dot notation such as user1.username.
Struct Update Syntax
You can create a new value from an existing struct with .. syntax:
let user2 = User {
email: String::from("new_email@example.com"),
..user1
};Fields not written explicitly come from user1. Be aware that non-Copy fields may move into user2, which can make all or part of user1 unavailable afterward.
Tuple Structs
Tuple structs are concise when field names would add little value:
struct Color(u8, u8, u8);
fn main() {
let red = Color(255, 0, 0);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
}The fields are accessed by position, such as red.0.
Unit Structs
A unit struct has no fields:
struct Unit;
fn main() {
let _u = Unit;
println!("Created a Unit struct value!");
}They are useful when the type itself conveys meaning even though no per-instance data is required.
Methods with impl
Methods are defined in an impl block:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
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!("rect1 area: {} pixels²", rect1.area());
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
}&self means the method borrows the struct immutably. Methods can also take &mut self to mutate the value or self to consume it.
Associated Functions
An impl block can also define functions that do not take self, commonly used as constructors:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}Call it with Rectangle::square(20).
Best Practices
- Use named fields when names make the data easier to understand.
- Use tuple structs when positional data is intentionally compact or when creating a newtype wrapper.
- Put behavior closely related to a type in its
implblocks. - Split very large data models into smaller types when they represent separate concepts.
- Use derives such as
Debug,Clone,Copy,PartialEq, orDefaultonly when their semantics make sense for the type.
Conclusion
Structs give Rust programs clear, strongly typed data models, while methods and associated functions keep behavior close to the data it operates on. Together they are a foundation for designing readable APIs and maintainable Rust applications.