Struct and Method Practice
Practice Rust by designing small types, adding methods, and choosing the right receiver: `&self`, `&mut self`, or `self`.
This is where Rust starts looking like real program design.
You are no longer just pushing values through helper functions.
Now you are deciding:
- what data belongs together
- what behavior belongs on that data
- which methods only read
- which methods mutate
- which methods should consume the value entirely
That is what these exercises are testing.
Do these in order.
The first exercise is about struct shape. The second adds methods. The third forces you to choose the correct receiver type.
What You Are Practicing
By the end of this page, you should be able to:
- design a named-field struct that has a clear purpose
- instantiate that struct cleanly
- add methods with
impl - choose between
&self,&mut self, andself - use an associated function like
newwhen it improves clarity
If any of those still feels shaky, that is exactly what the exercises are for.
Exercise 1: Model A Wallet Cleanly
Start with the base skill.
Take related values and turn them into one named type.
What to build
Create a new Cargo project:
cargo new wallet_model
cd wallet_modelReplace src/main.rs with this starter:
#[derive(Debug)]
struct Wallet {
owner: String,
balance: u64,
frozen: bool,
}
fn main() {
let wallet = Wallet {
owner: String::from("alice"),
balance: 1_500_000,
frozen: false,
};
println!("{:?}", wallet);
}Why this exercise matters
This is the first real shift from scattered variables to deliberate data modeling.
You are saying that owner, balance, and frozen are not separate facts. They are one wallet.
Your job
- type the struct and run it with
cargo run - add one more field like
nonce: u32orlabel: String - print a couple of fields directly with dot syntax
- explain why this is better than storing the same data as unrelated variables
What success looks like
You should be able to say:
- what this struct represents
- why these fields belong together
- what the type means in plain language
If you cannot do that, the struct is probably not well designed yet.
Exercise 2: Add Methods That Match The Data
Now give the struct behavior.
A wallet should not just exist. It should do things.
What to build
Extend the same Wallet type with methods.
Use this starter:
#[derive(Debug)]
struct Wallet {
owner: String,
balance: u64,
frozen: bool,
}
impl Wallet {
fn deposit(&mut self, amount: u64) {
// write this
}
fn can_withdraw(&self, amount: u64) -> bool {
// write this
false
}
fn freeze(&mut self) {
// write this
}
}
fn main() {
let mut wallet = Wallet {
owner: String::from("alice"),
balance: 1_500_000,
frozen: false,
};
wallet.deposit(250_000);
println!("Can withdraw 500000? {}", wallet.can_withdraw(500_000));
wallet.freeze();
println!("Can withdraw 500000 after freeze? {}", wallet.can_withdraw(500_000));
}Why this exercise matters
This is where method receiver choices stop being abstract.
depositchanges state, so it needs&mut selfcan_withdrawonly reads state, so it should use&selffreezechanges state too, so it also needs&mut self
The method signature should match the behavior exactly.
Your job
- implement all three methods
- run the program and check the outputs
- add another read-only method like
summary(&self) -> String - explain why
can_withdrawshould not take&mut self
What success looks like
You should be able to explain each method signature in one sentence.
If the method reads, say that.
If it mutates, say that.
That clarity matters more than the syntax itself.
Exercise 3: Choose Between &self, &mut self, and self
This is the decision that separates “I saw the syntax” from “I understand the design.”
What to build
Take the same wallet and add:
- an associated function
new - a read-only method
summary - a consuming method
close
Use this starter:
#[derive(Debug)]
struct Wallet {
owner: String,
balance: u64,
frozen: bool,
}
impl Wallet {
fn new(owner: String) -> Self {
// write this
Self {
owner,
balance: 0,
frozen: false,
}
}
fn summary(&self) -> String {
// write this
String::new()
}
fn close(self) -> String {
// write this
String::new()
}
}
fn main() {
let wallet = Wallet::new(String::from("alice"));
println!("{}", wallet.summary());
let closing_message = wallet.close();
println!("{}", closing_message);
// wallet should not be usable here anymore
}Why this exercise matters
This exercise forces three different design choices.
newdoes not operate on an existing instance, so it takes noselfsummaryreads the wallet, so it uses&selfcloseconsumes the wallet, so it takesself
That is the cleanest possible way to understand receiver types.
Your job
- implement
new,summary, andclose - run the program
- uncomment or add code after
wallet.close()that tries to usewalletagain - read the compiler error and explain why it happens
What success looks like
You should be able to explain exactly why these three signatures are different.
That is the real point of the exercise.
How To Work Through These Exercises
Start with the type before the methods
First decide what fields belong together.
Only after that should you add behavior.
Make method signatures tell the truth
Do not choose &mut self just because it feels safer.
Use the narrowest receiver that matches what the method actually does.
Let the compiler teach ownership again
When you experiment with a consuming method like close(self), try using the value afterward on purpose.
The error message is part of the lesson.
Common Failure Modes
Turning every helper into a method
Not every function belongs on the struct.
Put behavior in impl only when it clearly belongs to the type.
Using &mut self too often
A method that only reads data should not demand mutable access.
That makes your API harder to use and hides the real behavior.
Forgetting that consuming methods destroy the old value
If a method takes self, the caller loses that instance afterward.
That is correct when you truly want ownership transfer.
Summary
These exercises were designed to make the lesson practical.
If you finished them honestly, you have now practiced:
- designing a struct
- grouping related data into one named type
- adding methods with
impl - choosing
&self,&mut self, andselfcorrectly - using an associated function to construct a value cleanly
That is the core of this lesson.
The next lessons can build on this only if this part is solid.