DaleSchool

Ownership in Practice

Intermediate15min

Learning Objectives

  • Diagnose the cause of ownership errors in code
  • Choose between move, borrow, and copy appropriately
  • Apply ownership rules to structs and enums

This lesson is exercise-driven. You'll combine everything from Lessons 09–13 — ownership, borrowing, structs, and enums — to solve practical problems.

Read buggy code, diagnose the cause, and fix it. Repeat until it clicks!

Level 1: Simple Move Errors

Problem 1-1

fn main() {
    let name = String::from("Alice");
    let greeting = name;
    println!("Name: {name}");
    println!("Greeting: {greeting}");
}
error[E0382]: borrow of moved value: `name`

Diagnosis: name was moved to greeting. After a move, the original variable can't be used.

Fix 1 — Use the new owner after the move:

fn main() {
    let name = String::from("Alice");
    let greeting = name;
    println!("Greeting: {greeting}");
}

Fix 2 — Create a copy:

fn main() {
    let name = String::from("Alice");
    let greeting = name.clone();
    println!("Name: {name}");
    println!("Greeting: {greeting}");
}

Problem 1-2

Fix the code below so it compiles — without using clone().

fn print_length(s: String) -> usize {
    println!("String: {s}");
    s.len()
}

fn main() {
    let word = String::from("Rust");
    let len = print_length(word);
    println!("{word}'s length: {len}");
}
Hint

Change the parameter type so the function doesn't take ownership. Use &str or &String.

Solution
fn print_length(s: &str) -> usize {
    println!("String: {s}");
    s.len()
}

fn main() {
    let word = String::from("Rust");
    let len = print_length(&word);
    println!("{word}'s length: {len}");
}

Level 2: Borrowing in Function Parameters

Problem 2-1

fn add_greeting(names: &Vec<String>) {
    for name in names {
        names.push(format!("Hello, {name}!"));
    }
}

Why doesn't this compile?

Diagnosis

names is borrowed as &Vec (immutable reference), but push() tries to modify the vector. You have read permission but are trying to edit!

On top of that, modifying a vector while iterating over it could corrupt memory. Rust catches this at compile time.

Problem 2-2

Fix the code below so it compiles.

fn longest(s1: String, s2: String) -> String {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let a = String::from("hello");
    let b = String::from("hi");
    let result = longest(a, b);
    println!("Longer string: {result}");
    println!("a = {a}");  // error!
}
Hint

Use borrowing so the function doesn't take ownership. The return type also needs to become a reference.

Solution
fn longest(s1: &str, s2: &str) -> &str {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let a = String::from("hello");
    let b = String::from("hi");
    let result = longest(&a, &b);
    println!("Longer string: {result}");
    println!("a = {a}");  // OK!
}

Note: This code may require lifetime annotations. That's covered in the next lesson!

Level 3: Ownership in Structs + Methods

Problem 3-1

#[derive(Debug)]
struct Playlist {
    name: String,
    songs: Vec<String>,
}

impl Playlist {
    fn add_song(self, song: String) {
        self.songs.push(song);
    }

    fn show(&self) {
        println!("--- {} ---", self.name);
        for song in &self.songs {
            println!("  {song}");
        }
    }
}

fn main() {
    let playlist = Playlist {
        name: String::from("My Playlist"),
        songs: vec![String::from("Song 1")],
    };

    playlist.add_song(String::from("Song 2"));
    playlist.show();
}

There are two errors. Find and fix them!

Hint
  1. add_song takes self by value — it takes ownership. Since you only need to modify, what form of self is appropriate?
  2. self.songs.push() requires self to be mutable.
  3. The playlist variable also needs to be mutable.
Solution
impl Playlist {
    fn add_song(&mut self, song: String) {
        self.songs.push(song);
    }
    // show stays the same
}

fn main() {
    let mut playlist = Playlist {
        name: String::from("My Playlist"),
        songs: vec![String::from("Song 1")],
    };

    playlist.add_song(String::from("Song 2"));
    playlist.show();
}

Change self to &mut self and declare playlist as mut.

Revisiting Phase 1 Code

In Lesson 06 you wrote code like this:

fn print_scores(scores: Vec<i32>) {
    for s in scores {
        println!("{s}");
    }
}

fn main() {
    let scores = vec![90, 85, 77];
    print_scores(scores.clone()); // fixed with clone()
    println!("Total: {} scores", scores.len());
}

Now you can fix it without clone():

fn print_scores(scores: &[i32]) {
    for s in scores {
        println!("{s}");
    }
}

fn main() {
    let scores = vec![90, 85, 77];
    print_scores(&scores);  // just borrow
    println!("Total: {} scores", scores.len());
}

The parameter changed to &[i32] (a slice reference). No copying needed. Now you know why clone() was necessary — and what the better alternative is.

Ownership Decision Cheat Sheet

When writing code, follow this flow:

Need to pass a value
  ├─ Read-only? → &T (immutable reference)
  ├─ Need to modify? → &mut T (mutable reference)
  ├─ Need ownership? → T (move)
  └─ Not sure? → Start with &, adjust if you get errors

Exercise 1 (easy). Fix the code below so it compiles.

fn main() {
    let mut items = vec!["apple", "banana"];
    let first = &items[0];
    items.push("grape");
    println!("First: {first}");
}

Exercise 2 (medium). Complete the Student struct. add_score adds a score and average returns the mean.

#[derive(Debug)]
struct Student {
    name: String,
    scores: Vec<f64>,
}

impl Student {
    fn new(name: &str) -> Student {
        // Complete this
    }

    fn add_score(/* ??? */, score: f64) {
        // Complete this
    }

    fn average(/* ??? */) -> f64 {
        // Complete this
        // Hint: return 0.0 if scores is empty
    }
}

fn main() {
    let mut student = Student::new("Alice");
    student.add_score(90.0);
    student.add_score(85.0);
    student.add_score(92.0);
    println!("{}: average {:.1}", student.name, student.average());
}

Exercise 3 (challenge). Use the enum and functions below to build a simple calculator.

enum Operation {
    Add(f64, f64),
    Subtract(f64, f64),
    Multiply(f64, f64),
}

fn calculate(op: &Operation) -> f64 {
    // Use match to return the result of each operation
}

fn describe(op: &Operation) -> String {
    // Return a string like "3 + 5 = 8"
}

Q1. When a function only reads a value, the most appropriate parameter type is:

  • A) String
  • B) &str
  • C) mut String
  • D) String::from()

Q2. Why does this code produce an error?

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("{first}");
}
  • A) Vectors can't hold more than 3 elements
  • B) An immutable reference (first) exists while the vector is being modified
  • C) You can't pass a reference to println!
  • D) Vectors created with vec! can't be modified

Q3. Why is borrowing (&) better than clone()?

  • A) The code is shorter
  • B) It doesn't copy data, saving memory and time
  • C) Errors never occur
  • D) It works on every type