🦀 Rust Structs: Methods and Impl Blocks
The Restaurant Kitchen Analogy 🍳
Imagine you own a tiny restaurant. You have a recipe card (that’s your struct) with all the ingredients listed. But a recipe card just sitting there doesn’t cook anything!
You need chefs who know how to use that recipe. In Rust, those chefs are called methods, and they live inside a special kitchen called an impl block.
What is an impl Block?
An impl block is like a kitchen attached to your recipe. It’s where you write all the actions (methods) that work with your struct.
struct Pizza {
topping: String,
size: u8,
}
impl Pizza {
// Methods go here!
}
Think of it this way:
struct Pizza= The recipe card listing ingredientsimpl Pizza= The kitchen where chefs cook pizzas
Methods: Actions Your Struct Can Do
A method is a function that belongs to a struct. It can read or change the struct’s data.
The Magic Word: self
Every method gets a special helper called self. It’s like saying “this pizza” or “the pizza we’re working on right now.”
impl Pizza {
fn describe(&self) {
println!(
"A {} inch pizza with {}",
self.size,
self.topping
);
}
}
Breaking it down:
&self= “Let me look at this pizza” (borrow, read-only)self.size= “The size of THIS pizza”self.topping= “The topping on THIS pizza”
Using Your Method
fn main() {
let my_pizza = Pizza {
topping: String::from("pepperoni"),
size: 12,
};
my_pizza.describe();
// Prints: A 12 inch pizza with pepperoni
}
Notice the dot! We call my_pizza.describe() just like calling a friend: “Hey pizza, describe yourself!”
The Three Flavors of self
Think of borrowing a book from a library:
| Type | Meaning | Real-Life Example |
|---|---|---|
&self |
“I’ll just look” | Reading a library book |
&mut self |
“I’ll make notes” | Editing your own book |
self |
“I’m taking it” | Buying the book (it’s yours now) |
Example: All Three in Action
struct Cookie {
bites_left: u8,
}
impl Cookie {
// Just looking (read-only)
fn check(&self) -> u8 {
self.bites_left
}
// Making changes
fn take_bite(&mut self) {
if self.bites_left > 0 {
self.bites_left -= 1;
}
}
// Eating the whole thing!
fn eat(self) {
println!("Yum! Cookie is gone!");
// Cookie is consumed here
}
}
Associated Functions: The Factory Workers
Sometimes you need a function that makes a struct, not one that uses it.
These are called associated functions. They don’t use self because there’s no struct yet—they’re building one!
impl Pizza {
// Associated function (no self!)
fn new(topping: String) -> Pizza {
Pizza {
topping,
size: 10, // Default size
}
}
}
Calling Associated Functions
Notice the double colon ::? That’s how you call functions that belong to the type itself, not an instance.
fn main() {
// Using ::new() to create a pizza
let cheese_pizza = Pizza::new(
String::from("cheese")
);
// Now we can use methods with .
cheese_pizza.describe();
}
The difference:
Pizza::new()→ Calling the factory (makes a pizza)pizza.describe()→ Asking a pizza to do something
Quick Comparison
graph TD A["impl Block"] --> B["Methods"] A --> C["Associated Functions"] B --> D["Use self<br/>Call with .dot"] C --> E["No self<br/>Call with ::colons"] D --> F["pizza.describe#40;#41;"] E --> G["Pizza::new#40;#41;"]
The Self Type (Capital S!)
There’s a handy shortcut: Self (with a capital S) means “the type we’re implementing for.”
impl Pizza {
fn new(topping: String) -> Self {
Self {
topping,
size: 10,
}
}
}
Self = Pizza here. It’s like saying “make one of me!”
Why use it?
- Less typing
- If you rename the struct, the code still works
- Makes copy-pasting easier
Multiple impl Blocks
You can split methods into separate blocks! This is useful for organization.
struct Robot {
name: String,
battery: u8,
}
// Movement methods
impl Robot {
fn walk(&mut self) {
self.battery -= 5;
println!("{} walks!", self.name);
}
}
// Factory methods
impl Robot {
fn new(name: String) -> Self {
Self { name, battery: 100 }
}
}
Both blocks work together—Rust combines them automatically!
Putting It All Together 🎉
Let’s build a complete example:
struct BankAccount {
owner: String,
balance: u32,
}
impl BankAccount {
// Associated function: create account
fn new(owner: String) -> Self {
Self { owner, balance: 0 }
}
// Method: check balance (read)
fn check_balance(&self) -> u32 {
self.balance
}
// Method: deposit (modify)
fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
// Method: withdraw (modify)
fn withdraw(&mut self, amount: u32) -> bool {
if amount <= self.balance {
self.balance -= amount;
true
} else {
false
}
}
}
fn main() {
// Create with ::
let mut account = BankAccount::new(
String::from("Alice")
);
// Use methods with .
account.deposit(100);
account.withdraw(30);
println!(
"Balance: ${}",
account.check_balance()
);
// Prints: Balance: $70
}
Key Takeaways 🔑
| Concept | What It Means | Example |
|---|---|---|
impl block |
Kitchen for methods | impl Pizza { } |
| Method | Action with self |
fn eat(&self) |
| Associated function | No self, creates things |
fn new() -> Self |
&self |
Borrow, read-only | Looking at data |
&mut self |
Borrow, can modify | Changing data |
self |
Take ownership | Consuming the struct |
Self |
The type itself | Shortcut in impl |
. dot |
Call methods | pizza.eat() |
:: colons |
Call associated funcs | Pizza::new() |
You Did It! 🎊
Now you know how to give your structs superpowers:
- Wrap them in
implblocks - Add methods with
selffor actions - Add associated functions for creating new instances
- Use the right
selftype based on what you need
Go build something awesome! 🚀
