8.xChapter 8 summary and quiz
The chapter the course was named after is behind you. If it felt smaller than its reputation, that was the plan: seven short lessons, each one move on the same diagram. Here's the whole picture at once, then a quiz that makes you play it.
Quick review
Memory that a program requests at runtime must be given back exactly once; too early is a use-after-free, twice is a double free, never is a leak. The industry's three answers: trust the programmer (C/C++; fast and dangerous), pay a garbage collector (Java and friends; safe, with runtime cost and unpredictable pauses), or Rust's third way, ownership: compile-time rules that let the compiler write every give-it-back into the program before it runs. The rules fit in a breath: every value has an owner; one owner at a time; when the owner goes out of scope, the value is dropped.
The floor plan underneath: the stack is a fixed-size column of memory run with plate-stack discipline, holding one frame per unreturned function call (return bookmark, parameters, locals). Pushing and popping frames is just moving a marker, which is why stack allocation is nearly free, why locals never needed cleanup ceremony, and why deep recursion ends in a stack overflow. The stack's price is rigidity: every size known at compile time. Runtime-sized data lives on the heap, where the allocator hands out blocks on request. A String straddles both: a fixed-size handle in the frame (address, length, capacity) owning a heap block of text.
Dropped means the value's memory is given back at the owner's closing brace: a fixed place in the program, not a background process. Cleanup you can point at is the whole trick; it's how Rust gets collector-grade safety with zero collector.
Moves are what = means for owning types: the handle transfers, the old variable is dead, and using it is error[E0382], whose anatomy you now read fluently (move-because-the-type-isn't-Copy, value moved here, borrowed here after move, and a .clone() diff if the compiler thinks it'd help). One owner at a time makes the double free unwritable. Copy types opt out because they live whole in their boxes (all integers, floats, bool, char, unit, all-Copy tuples, and &str, which copies only the view): for them = copies and both variables live. Functions follow the same physics: passing a String gives it, the parameter drops it on return, and the compiler's own note says which parameter "takes ownership of the value"; returning a value gives it back. Functions that only read text take &str; functions that keep it take String; and + consumes its left side because it owns, grows, and hands onward that very block. When two independent values is the genuine requirement, .clone() buys the deep copy with visible ink: the expensive thing in Rust is never silent, and the silent thing is never expensive.
And one promissory note, now due next chapter: the give-and-take-back tuple dance, the error-silencing clone, and the note "consider changing this parameter type to borrow instead" all point at the same door. You've been pushing values through it with &mut since lesson 1.6. Chapter 9 opens it.
Quiz time
Question #1
Three claims, true or false, one sentence of why:
a) When a Rust program assigns one String variable to another, the text is copied to a new heap block.
b) A value can be unusable while its variable's name is still in scope.
c) Dropping happens when Rust's runtime detects a value is no longer reachable.
Show solution
a) False: the handle moves and the block stays put; copying the text is exactly what = won't do silently (that's .clone()).
b) True: that's a moved-from variable, lesson 8.4's warning box; scope governs the name, ownership governs the value.
c) False, twice over: nothing is detected and there is no collector; the drop point is the owner's closing brace, fixed at compile time.
Question #2
Compile or not? For each, one sentence.
a)
fn main() {
let x = 7;
let y = x;
println!("{x} {y}");
}
b)
fn main() {
let tag = String::from("fragile");
let label = tag;
println!("{tag}");
}
c)
fn main() {
let park = String::from("bench");
let view: &str = &park;
let second = view;
println!("{view} {second} {park}");
}
d)
fn main() {
let s = String::from("inner");
{
let t = s;
}
println!("{s}");
}Show solution
a) Compiles (7 7): i32 is Copy, so = copies and both live.
b) error[E0382]: String moves to label, and tag is used after its value left.
c) Compiles (bench bench bench): &str is Copy, so the view duplicates freely, and neither view affects park's ownership.
d) error[E0382]: the value moves into the block's t and is dropped at the inner brace; the final line uses s, whose value is long gone even though its name is in scope.
Question #3
Read this error aloud, the way lesson 8.4 did: say which line moved the value, which line used it illegally, and what the compiler's two suggestions (one note, one help) would each mean for the program.
fn main() {
let recipe = String::from("knead, rest, bake");
publish(recipe);
println!("saved: {recipe}");
}
fn publish(text: String) {
println!("publishing: {text}");
}error[E0382]: borrow of moved value: `recipe`
--> src/main.rs:4:23
|
2 | let recipe = String::from("knead, rest, bake");
| ------ move occurs because `recipe` has type `String`, which does not implement the `Copy` trait
3 | publish(recipe);
| ------ value moved here
4 | println!("saved: {recipe}");
| ^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `publish` to borrow instead if owning the value isn't necessary
--> src/main.rs:7:18
|
7 | fn publish(text: String) {
| ------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
3 | publish(recipe.clone());
| ++++++++Show solution
Line 3 moved the value: calling publish(recipe) gives the String to the text parameter, which drops it when publish returns. Line 4 uses it illegally: recipe is moved-from, a name owning nothing. The note proposes fixing the signature: if publish only reads the text (it does; it prints), it should borrow rather than take ownership, which is chapter 9's subject and the better fix. The help proposes fixing the call site: recipe.clone() would hand publish a deep copy and keep the original, at the price of duplicating the text; legal, but paying an allocation to avoid a &.
Question #4
Bug hunt. This program compiles and works, but a chapter 8 graduate should wince three times. Find the three wasteful or wrong-headed spots and say what each should be:
fn banner(title: String) -> String {
format!("== {title} ==")
}
fn main() {
let title = String::from("Daily Report");
let decorated = banner(title.clone());
println!("{decorated}");
println!("{}", title.clone());
}Show solution
First: banner takes String but only reads the title to build its result; it should take &str (the function never keeps the text, so it shouldn't take ownership). Second: banner(title.clone()) is the error-silencing clone from lesson 8.7, duplicating the whole text to feed a function that shouldn't be consuming it; fix the signature and pass &title. Third: title.clone() in the last line clones for no reason at all; printing only reads the value, and println!("{title}") was already legal. Total honest cost of the fixed program: zero clones.
Question #5
Write a gift-tag program:
- An
askfunction: takes a prompt as&str, prints it, reads a line (lesson 1.6's recipe), and returns the trimmed input as an ownedString. Explain to yourself why the return type must beStringand not a view of the input. - A
make_tagfunction: takes a recipient and a gift as&strs plus a count asu32, and returns aStringlikeTo Maria: 3 x teacups(lesson 5.5's tools). - A
with_lengthfunction, just to feel this chapter in your fingers: takes the finished tag by value (String) and returns(String, usize), the tag and its length in bytes, give-and-take-back style. mainwires it together, asks for recipient, gift, and count (parse the count, lesson 5.6), and prints the tag plus its byte length.
A run should look like:
Who is the gift for?
What is the gift?
How many?
To Maria: 3 x teacups
(21 bytes)Show solution
use std::io;
fn ask(prompt: &str) -> String {
println!("{prompt}");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("failed to read input");
String::from(input.trim())
}
fn make_tag(recipient: &str, gift: &str, count: u32) -> String {
format!("To {recipient}: {count} x {gift}")
}
fn with_length(tag: String) -> (String, usize) {
let bytes = tag.len();
(tag, bytes)
}
fn main() {
let recipient = ask("Who is the gift for?");
let gift = ask("What is the gift?");
let count: u32 = ask("How many?").parse().expect("not a number");
let tag = make_tag(&recipient, &gift, count);
let (tag, bytes) = with_length(tag);
println!("{tag}");
println!("({bytes} bytes)");
}
The ownership tour, function by function: ask must return an owned String because its local input dies at its closing brace, so any view into it could not survive the return (chapter 9 turns that intuition into a compiler-enforced law); building a fresh String and moving it out is the correct shape. make_tag takes &strs because it only reads its inputs (&recipient lends the text, lesson 5.4's auto-conversion at work), takes count by Copy, and gives the caller ownership of the format! result. with_length is the deliberately awkward one: it takes the tag by value, so main must catch it again from the returned tuple via a shadowing let. After chapter 9 you'd write it as a &str parameter (or just call .len() directly) and delete the dance. The count parse is ask("How many?").parse(): the String returned by ask is consumed by the expression, dropped after parse reads it, which is fine, because nobody needed it afterwards.
What's next
Chapter 9 keeps the biggest promise in the course. Every awkward moment in this chapter (the tuple dance, the defensive clone, the function that swallowed your String) dissolves into a single character you've been writing since lesson 1.6: &. References, borrowing, and the famous borrow checker, which you'll find is not a wall but the chapter 8 rules with better manners.