This lesson is a turning point in the curriculum. You'll take code that "just works" from Phase 1 and polish it into idiomatic Rust using the tools from Phase 3.
You don't need to write perfect code from the start. Write it to work first, then refine it. That's the natural flow of Rust development.
Pattern 1: clone() -> Borrowing (&)
Before
In Lesson 06, passing a vector to a function moved ownership, so you used clone():
fn print_list(list: Vec<String>) {
for item in &list {
println!("- {item}");
}
}
fn main() {
let fruits = vec![
String::from("apple"),
String::from("banana"),
String::from("grape"),
];
print_list(fruits.clone()); // clone to copy!
println!("Total: {}", fruits.len());
}
After
Now you know borrowing. Use &[String] and the copy cost disappears:
fn print_list(list: &[String]) {
for item in list {
println!("- {item}");
}
}
fn main() {
let fruits = vec![
String::from("apple"),
String::from("banana"),
String::from("grape"),
];
print_list(&fruits); // just borrow!
println!("Total: {}", fruits.len());
}
What Changed?
| | Before | After |
| ---------------- | ------------------------------- | ----------------------------- |
| Parameter | Vec<String> (ownership moves) | &[String] (slice reference) |
| Call site | fruits.clone() | &fruits |
| Memory cost | Entire vector copied | One pointer passed |
| Original usable? | Yes, thanks to clone() | Yes, naturally |
Pattern 2: unwrap() -> ? Operator
Before
The number-guessing game from Lesson 07 used unwrap():
use std::io;
fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap(); // crashes on error
let number: i32 = input.trim().parse().unwrap(); // crashes on error
println!("You entered: {number}");
}
Two unwrap() calls. Any failure kills the program.
After
The ? operator handles errors gracefully:
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn read_number() -> Result<i32, AppError> {
let mut input = String::new();
io::stdin().read_line(&mut input)?; // propagate error with ?
let number: i32 = input.trim().parse()?; // propagate error with ?
Ok(number)
}
fn main() {
match read_number() {
Ok(n) => println!("You entered: {n}"),
Err(e) => println!("Error: {:?}", e),
}
}
Simpler Alternative: Box<dyn Error>
If defining a custom error type is too much overhead, use Box<dyn std::error::Error>:
use std::io;
fn read_number() -> Result<i32, Box<dyn std::error::Error>> {
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let number: i32 = input.trim().parse()?;
Ok(number)
}
fn main() {
match read_number() {
Ok(n) => println!("You entered: {n}"),
Err(e) => println!("Error: {e}"),
}
}
For simple programs, this approach is convenient!
What Changed?
| | Before | After |
| ------------------ | ------------------- | --------------------------------- |
| Error handling | unwrap() — panic! | ? — error propagation |
| Stability | Crashes on error | Handles errors gracefully |
| Function signature | fn main() | fn read_number() -> Result<...> |
| Readability | Short but dangerous | Slightly longer but safe |
Pattern 3: for Loop -> Iterator Chain
Before
Score processing with for loops in Lesson 06:
fn main() {
let scores = vec![45, 82, 67, 93, 55, 78, 88];
// Count passing scores (>= 60)
let mut pass_count = 0;
for score in &scores {
if *score >= 60 {
pass_count += 1;
}
}
// Average of passing scores
let mut pass_total = 0;
for score in &scores {
if *score >= 60 {
pass_total += score;
}
}
let pass_avg = pass_total as f64 / pass_count as f64;
println!("Passing: {pass_count}");
println!("Average: {pass_avg:.1}");
}
After
Iterator chains make the intent clearer:
fn main() {
let scores = vec![45, 82, 67, 93, 55, 78, 88];
let passing: Vec<&i32> = scores.iter().filter(|s| **s >= 60).collect();
let pass_count = passing.len();
let pass_avg = passing.iter().map(|s| **s as f64).sum::<f64>() / pass_count as f64;
println!("Passing: {pass_count}");
println!("Average: {pass_avg:.1}");
}
What Changed?
| | Before | After |
| ----------------- | ---------------------- | ------------------------- |
| Iterations | Vector traversed twice | Filter once + sum |
| Mutable variables | 2 mut needed | None |
| Intent | Must read inside loops | Chain reads top to bottom |
Refactoring Checklist
Review your code against this list:
- [ ] Any
clone()calls? -> Check if borrowing (&) works instead - [ ] Any
unwrap()calls? -> Replace with?,match, orunwrap_or() - [ ] Building a Vec with
push()in aforloop? -> Considermap().collect() - [ ] Filtering inside a
forloop? -> Considerfilter() - [ ] Accumulating a value in a
forloop? -> Considerfold()orsum() - [ ] Many
let mutvariables? -> Check if iterator chains can reduce them
Common Mistakes
1. You Don't Need to Remove Every clone()
// clone() is appropriate here
let config = load_config();
let backup = config.clone(); // you actually need a backup copy
modify_config(config);
When you genuinely need to copy data, clone() is correct. Only remove unnecessary clones.
2. Iterator Chains Aren't Always Better
// Overly long chains are hard to read
let result = data.iter()
.filter(|x| x.is_valid())
.map(|x| x.transform())
.flat_map(|x| x.children())
.filter(|c| c.score > 0)
.map(|c| c.name.clone())
.take(5)
.collect::<Vec<_>>();
// Splitting into intermediate variables can be clearer
let valid_items: Vec<_> = data.iter().filter(|x| x.is_valid()).collect();
let result: Vec<_> = valid_items.iter()
.flat_map(|x| x.children())
.filter(|c| c.score > 0)
.map(|c| c.name.clone())
.take(5)
.collect();
If readability suffers, break the chain!
Exercise 1. Remove the clone() calls from this code.
fn greet(name: String) {
println!("Hello, {name}!");
}
fn main() {
let name = String::from("Alice");
greet(name.clone());
greet(name.clone());
println!("Name: {name}");
}
Hint
greet doesn't need ownership. Change the parameter to &str.
Exercise 2. Refactor this for loop into an iterator chain.
fn main() {
let words = vec!["hello", "hi", "rust", "programming", "go", "iterator"];
let mut long_words = Vec::new();
for word in &words {
if word.len() >= 4 {
long_words.push(word.to_uppercase());
}
}
println!("{:?}", long_words);
}
Exercise 3 (challenge). Refactor this function to eliminate unwrap(). Change it to return a Result.
fn parse_scores(input: &str) -> Vec<i32> {
let mut scores = Vec::new();
for part in input.split(',') {
let score: i32 = part.trim().parse().unwrap();
scores.push(score);
}
scores
}
fn main() {
let data = "90, 85, 77, 92";
let scores = parse_scores(data);
println!("{:?}", scores);
}
Hint
Change the return type to Result<Vec<i32>, std::num::ParseIntError> and use ?.
:::
Q1. Why is replacing clone() with borrowing (&) beneficial?
- A) The code gets shorter
- B) It avoids copying data, saving memory and time
- C) It reduces compile time
- D) It always works for every type
Q2. Which statement correctly describes the difference between unwrap() and ??
- A)
unwrap()ignores errors;?prints them - B)
unwrap()panics on error;?propagates the error to the caller - C)
?is slower - D) No difference
Q3. Which iterator chain is equivalent to this for loop?
let mut result = Vec::new();
for n in &numbers {
if *n > 0 {
result.push(n * 2);
}
}
- A)
numbers.iter().map(|n| n * 2).collect() - B)
numbers.iter().filter(|n| **n > 0).map(|n| n * 2).collect() - C)
numbers.iter().filter(|n| **n > 0).collect() - D)
numbers.into_iter().map(|n| n * 2).filter(|n| *n > 0).collect()