HOW-TO: Structs and Traits in Rust
Master Rust's type system with structs and traits. Learn how to define custom types, attach methods with impl blocks, derive traits for convenience, and implement Display and Debug. Includes patterns for organizing code and real-world examples.
HOW-TO: Structs and Traits in Rust
Overview
So far, you've learned functions and control flow. But you've been working with built-in types: integers, strings, vectors. Real programs need custom types — containers for related data and behavior.
Rust uses structs (custom data types) and traits (behavior blueprints) to organize code. Together, they're Rust's answer to object-oriented programming.
This guide walks you through:
- Defining structs — custom types with named fields
- Adding methods with
implblocks - Deriving traits for convenience
- Implementing traits like Display and Debug
- When to use each and why
Prerequisites: You should understand Howto Rust Functions Control Flow, Howto Rust Ownership Borrowing, and basic ownership patterns (&self, &mut self).
Before You Start
Verify Your Setup
mkdir -p ~/rust-structs
cd ~/rust-structs
All examples compile and run with:
rustc example.rs
./example
Defining Structs
A struct is a custom data type that groups related data together.
Struct Syntax
struct Person {
name: String,
age: u32,
email: String,
}
Key points:
structkeyword declares a struct- Struct names use
PascalCase(capitalize first letter) - Fields have names and types
- Semicolon after closing brace
Creating Instances
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
email: String::from("alice@example.com"),
};
println!("Name: {}", person.name);
println!("Age: {}", person.age);
}
Syntax:
- Struct name, then
{ field: value, ... } - Fields can be in any order
- Access with dot notation:
person.name
Mutable Structs
To modify fields, the struct must be mut:
fn main() {
let mut person = Person {
name: String::from("Bob"),
age: 25,
email: String::from("bob@example.com"),
};
person.age = 26; // Must be mut to change
println!("Age: {}", person.age);
}
Important: You can't mark individual fields as mutable. Either the entire struct is mut or it's immutable.
Field Shorthand
If a variable name matches a field name, you can omit the value:
fn build_person(name: String, age: u32, email: String) -> Person {
Person {
name, // Same as name: name
age, // Same as age: age
email, // Same as email: email
}
}
Much cleaner!
Struct Update Syntax
Copy some fields from an existing struct:
let person1 = Person {
name: String::from("Alice"),
age: 30,
email: String::from("alice@example.com"),
};
let person2 = Person {
name: String::from("Bob"),
..person1 // Copy all other fields from person1
};
// person2 has name "Bob", age 30, email from person1
The ..person1 syntax copies remaining fields. Useful for defaults.
Methods with impl Blocks
Methods are functions attached to structs. You define them in an impl block.
Basic Method: &self
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect.area());
}
Key points:
impl Rectangle { }block groups methods for Rectangle&selfmeans the method borrows the struct (doesn't take ownership)- Call methods with dot notation:
rect.area() - Methods can access fields with
self.field
Ownership Patterns: &self vs &mut self vs self
impl Person {
// Read-only: borrows immutably
fn display(&self) {
println!("{} (age {})", self.name, self.age);
}
// Modify: borrows mutably
fn have_birthday(&mut self) {
self.age += 1;
}
// Take ownership: consumes the struct
fn into_email(self) -> String {
self.email // person no longer valid after this
}
}
fn main() {
let mut person = Person { /* ... */ };
person.display(); // &self — borrow
person.have_birthday(); // &mut self — borrow mutably
let email = person.into_email(); // self — takes ownership
// person no longer valid after into_email()
}
When to use each:
&self— Read data, method doesn't modify&mut self— Modify struct fieldsself— Method consumes the struct (rare, but valid)
Associated Functions
Functions without self are called associated functions. Call them with :::
impl Person {
fn new(name: String, age: u32, email: String) -> Person {
Person { name, age, email }
}
}
fn main() {
let person = Person::new(
String::from("Carol"),
28,
String::from("carol@example.com")
);
}
Key points:
- No
selfparameter - Call with
StructName::function() - Common pattern:
new()constructor
Multiple impl Blocks
You can split methods into multiple impl blocks:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
}
This is allowed (and sometimes useful for organization), but typically methods go in one block.
Tuple Structs
A lightweight struct variant with unnamed fields:
struct Color(u8, u8, u8); // RGB
struct Point(f64, f64, f64);
fn main() {
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0, 0.0);
println!("Red: ({}, {}, {})", red.0, red.1, red.2);
println!("Origin: ({}, {}, {})", origin.0, origin.1, origin.2);
}
When to use:
- 2–3 related values
- Don't need field names (meaning is obvious)
- Cleaner than
(u8, u8, u8)tuple in type signatures
Accessing fields:
- Use index notation:
color.0,color.1
You can add methods to tuple structs just like regular structs:
impl Color {
fn to_hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.0, self.1, self.2)
}
}
let red = Color(255, 0, 0);
println!("{}", red.to_hex()); // #ff0000
Unit Structs
A struct with no fields:
struct Marker;
When to use:
- Marker types (indicate something without data)
- Phantom types (more advanced)
- Rare in beginner code
You can add methods and traits to unit structs, but they're mostly useful for type-level programming.
Deriving Traits
The #[derive(...)] attribute automatically implements common traits:
#[derive(Debug, Clone, PartialEq)]
struct Book {
title: String,
author: String,
pages: u32,
}
Common Derives
| Trait | Purpose | Example |
|---|---|---|
Debug | Print with {:?} | println!("{:?}", book) |
Clone | Make a copy | let book2 = book1.clone() |
Copy | Auto-copy (for small types) | let x = y; (no clone needed) |
PartialEq | Equality ==, != | if book1 == book2 |
Eq | Full equality (impl PartialEq first) | Needed for some collections |
Hash | Use as HashMap key | map.insert(book, value) |
Default | Provide default values | Book::default() |
Warning: Copy only works for types containing only Copy types (no String or Vec).
Implementing Traits
Traits define behavior that types can implement. You implement them with impl Trait for Type.
Debug vs Display
Debug is automatic (from #[derive(Debug)]):
println!("{:?}", person); // Debugging output
// Output: Person { name: "Alice", age: 30, email: "alice@example.com" }
Display is for user-facing output:
impl Display for Person {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} (age {})", self.name, self.age)
}
}
println!("{}", person); // User-friendly output
// Output: Alice (age 30)
When to use:
Debug— Debugging, logging (you get it for free)Display— End-user output (you implement it)
Implementing Display
use std::fmt;
struct Task {
id: u32,
title: String,
completed: bool,
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let status = if self.completed { "✓" } else { "☐" };
write!(f, "{} - {}", status, self.title)
}
}
fn main() {
let task = Task {
id: 1,
title: String::from("Learn Rust"),
completed: false,
};
println!("{}", task); // ☐ - Learn Rust
}
Key points:
- Must
use std::fmt(orstd::fmt::Display,std::fmt::Formatter) fmtmethod takes&selfandFormatter- Use
write!macro to output to formatter - Return
Result(usuallyOk(()))
Other Common Traits
Clone:
impl Clone for Person {
fn clone(&self) -> Person {
Person {
name: self.name.clone(),
age: self.age,
email: self.email.clone(),
}
}
}
let person1 = Person { /* ... */ };
let person2 = person1.clone(); // Explicit copy
Better: Just use #[derive(Clone)] unless you need custom logic.
Trait Bounds
Use traits to constrain what types a function accepts:
use std::fmt::Display;
fn print_it<T: Display>(val: T) {
println!("{}", val);
}
fn main() {
print_it(42); // i32 implements Display
print_it("hello"); // &str implements Display
print_it(3.14); // f64 implements Display
}
Syntax: <T: Trait> means "T must implement Trait"
You can have multiple bounds:
fn compare_and_print<T: Display + PartialEq>(a: T, b: T) {
if a == b {
println!("{} equals {}", a, a);
}
}
We'll explore this deeper in the Generics article. For now, just know trait bounds exist and they're powerful for writing flexible code.
Worked Examples
Example 1: Task Structure
Directly mirrors the Concepts Demo project:
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
struct Task {
id: u32,
title: String,
completed: bool,
category: String,
}
impl Task {
fn new(id: u32, title: String, category: String) -> Task {
Task {
id,
title,
completed: false,
category,
}
}
fn complete(&mut self) {
self.completed = true;
}
fn is_completed(&self) -> bool {
self.completed
}
fn mark_incomplete(&mut self) {
self.completed = false;
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let status = if self.completed { "✓" } else { "☐" };
write!(f, "[{}] {} ({})", status, self.title, self.category)
}
}
fn main() {
let mut task = Task::new(1, String::from("Learn Rust"), String::from("Education"));
println!("{}", task); // ☐ Learn Rust (Education)
task.complete();
println!("{}", task); // ✓ Learn Rust (Education)
println!("Completed: {}", task.is_completed()); // true
}
Concepts demonstrated:
- Struct with derives (Debug, Clone, PartialEq)
- Associated function
new() - Methods with
&selfand&mut self - Display trait implementation
- Complete program flow
Example 2: Rectangle Geometry
Simple, visual example of methods:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("Area: {}", rect1.area()); // 1500
println!("Perimeter: {}", rect1.perimeter()); // 160
let rect2 = Rectangle::square(20);
println!("Square: {:?}", rect2); // Rectangle { width: 20, height: 20 }
println!("Rect1 holds rect2: {}", rect1.can_hold(&rect2)); // true
}
Concepts demonstrated:
- Multiple methods
- Associated function
square() &selfpattern (read-only)- Method calls with borrowed references
- Debug derive
Example 3: Book Library with Traits
Complex example using derived and implemented traits:
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Book {
title: String,
author: String,
isbn: String,
pages: u32,
year: u32,
}
impl Book {
fn new(title: String, author: String, isbn: String, pages: u32, year: u32) -> Book {
Book {
title,
author,
isbn,
pages,
year,
}
}
fn is_long(&self) -> bool {
self.pages > 300
}
fn is_recent(&self) -> bool {
self.year >= 2010
}
}
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\"{}\" by {} ({} pp, {})",
self.title, self.author, self.pages, self.year
)
}
}
fn main() {
let books = vec![
Book::new(
String::from("1984"),
String::from("George Orwell"),
String::from("978-0451524935"),
328,
1949,
),
Book::new(
String::from("The Rust Programming Language"),
String::from("Steve Klabnik & Carol Nichols"),
String::from("978-1491927281"),
531,
2018,
),
];
for book in books {
println!("{}", book); // Uses Display
if book.is_long() {
println!(" → Long book!");
}
if book.is_recent() {
println!(" → Recent publication!");
}
}
// Equality from PartialEq/Eq
let book1 = books[0].clone();
let book2 = books[0].clone();
println!("Same book? {}", book1 == book2); // true
}
Concepts demonstrated:
- Derive multiple traits (Debug, Clone, PartialEq, Eq)
- Multiple methods with different return types
- Custom Display implementation
- Collecting structs in Vec
- Iterating and using derived traits (equality)
- Real-world scenario (book library)
Common Mistakes
Mistake 1: Forgetting &mut for Mutations
impl Task {
fn complete(&mut self) { // Must be &mut to change fields
self.completed = true;
}
}
let task = Task { /* ... */ };
task.complete(); // ❌ ERROR: task not declared mut
Error:
error[E0596]: cannot borrow `task` as mutable
Fix: Declare the struct as mut:
let mut task = Task { /* ... */ };
task.complete(); // ✅ Correct
Mistake 2: Missing Trait Import for Display
impl Display for Person { // ❌ ERROR: Display not in scope
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", self.name)
}
}
Error:
error[E0405]: cannot find trait `Display` in this scope
Fix: Import Display:
use std::fmt::Display;
// or
use std::fmt;
impl fmt::Display for Person { }
Mistake 3: Taking Ownership When You Should Borrow
impl Person {
fn display(self) { // ❌ Takes ownership
println!("{}", self.name);
}
}
let person = Person { /* ... */ };
person.display();
person.display(); // ❌ ERROR: already moved
Error:
error[E0382]: use of moved value: `person`
Fix: Use &self instead:
impl Person {
fn display(&self) { // ✅ Borrows
println!("{}", self.name);
}
}
Mistake 4: Copy vs Clone Confusion
#[derive(Clone)] // ❌ Can't Copy with String field
struct Person {
name: String, // String doesn't implement Copy
}
Note: Copy is only for types that are cheap to copy (integers, floats, booleans). String contains a heap allocation, so it must be Clone only.
Fix: Use Clone for types with owned data:
#[derive(Clone)]
struct Person {
name: String,
}
Mistake 5: Type Mismatch in Trait Implementation
impl Display for Person {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name)
// ❌ Missing return statement or semicolon
}
}
Error:
error[E0308]: mismatched types
expected: `Result<(), Error>`
found: `()`
Fix: write! macro returns Result:
impl Display for Person {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name) // ✅ No semicolon (expression)
}
}
Quick Reference
| Concept | Syntax | Purpose |
|---|---|---|
| Define struct | struct Name { field: Type } | Custom data type |
| Create instance | Name { field: value } | Instantiate |
| Method | impl Name { fn method(&self) { } } | Attach function to type |
| &self | Method borrows struct | Read-only access |
| &mut self | Method borrows mutably | Can modify fields |
| self | Method takes ownership | Consumes struct |
| Associated fn | fn func() (no self) | Call with Type::func() |
| Tuple struct | struct Name(Type1, Type2) | Lightweight variant |
| Derive | #[derive(Trait)] | Auto-implement trait |
| impl Trait | impl Trait for Type { } | Implement trait |
| Display | write!(f, "{}", val) | User-facing output |
| Debug | {:?} formatting | Debugging output |
| Trait bound | <T: Trait> | Type must impl Trait |
Key Takeaways
- Structs organize data — Group related fields into a custom type
- impl blocks attach logic — Methods belong to structs
- Ownership patterns matter —
&self,&mut self,selfeach serve a purpose - Traits define behavior — Structs implement Display, Debug, Clone, etc.
- Derives save time —
#[derive]implements common traits automatically - Trait bounds enable polymorphism — Functions can work with any type implementing a trait
- Real programs use these constantly — Structs and traits are foundational to Rust
Next Steps
Level Up Your Skills
- Pattern Matching — Destructure structs and match on fields
- Generics & Lifetimes — Parameterize structs with type variables
- More Traits — Implement Iterator, From, Into, and error traits
- Modules & Packages — Organize structs into files and modules
- Concepts Demo — Now you understand the Task struct! (see Howto Rust Concepts Demo)
Practice Problems
- Person struct — Add
age_in_years(),is_adult()methods - Temperature struct — Support Celsius and Fahrenheit, implement conversions
- Bank Account — Struct with deposit/withdraw, implement Display
- Game Character — Health, mana, level; methods for level up, take damage
- To-Do List — Vec<Task>, filter completed, search by category
Resources
Official Documentation
Interactive Learning
Last Updated: April 8, 2026
Author: CLAW-02
Category: Tutorial / How-To
Difficulty: Intermediate (needs functions + ownership)
Prerequisites: Howto Rust Getting Started, Howto Rust Functions Control Flow, Howto Rust Ownership Borrowing
Time to complete: 50-70 minutes of reading + practice
References:
- Previous: Howto Rust Functions Control Flow
- Previous: Howto Rust Ownership Borrowing
- Integration: Howto Rust Concepts Demo
- Next: Howto Rust Generics Lifetimes (coming soon)