5.xChapter 5 summary and quiz

Last updated June 12, 2026

This was the chapter where text stopped being a prop and became a subject, and where two ideas (constants and shadowing) joined the permanent toolkit. The review, a bug hunt, and a program that needs all of it.

Quick review

A constant is declared with const, requires a type annotation, lives in any scope including global, and must have a value the compiler can settle at compile time: literals and arithmetic on them yes, anything runtime (user input, ordinary variables) no, enforced by E0435. Constants are named in SCREAMING_SNAKE_CASE, and the compiler warns you personally if you forget. They're the cure for magic numbers: bare literals whose meaning and update-sites nobody can reconstruct later. Name what isn't obvious; leave obvious conversion factors alone. A static is a fixed value with one address for the whole program; until an address matters (chapters 21 and 25), prefer const.

Shadowing is a second let reusing a name: a new variable steps in front of the old, no mutation occurs, no mut is needed, and the old one becomes unreachable rather than changed. A shadow ends with its scope (block shadows don't leak out, unlike real assignment), and unlike a mut rebind, a shadow may change type, which is the trick behind the course's favorite line: let age: u32 = age.trim().parse().expect(...), one concept keeping one name across three types.

Text comes in two flavors with one relationship. A String owns its text: growable (push_str for text, push for a single-quoted char), runtime-sized, created via String::new(), String::from(...), or .to_string(). A &str is a borrowed view of text owned elsewhere: every string literal is one, and so is trim's return value, a copy-free window into the String it came from (the owner/viewer split runs on chapter 9's machinery; views that outlive their text don't compile at all, and chapter 9 explains why). Store text as String, accept parameters as &str, lend with & at the call site. And len() counts bytes, not characters: "café" is 4 characters, 5 bytes.

format! is println! returning the String instead of printing it, and both speak the brace mini-language: width {:8}, alignment {:<} {:>} {:^} (fill character optional: {:->8}), float precision {:.2} (display-side rounding only), zero-padding {:08}, forced sign {:+}, all combinable as in {total:>7.2}. {:?} asks for the debug view, made for programmers: tuples print, strings wear their quotes and show their \ns, and dbg! has been using it all along. parse crosses the text/number border deliberately: it targets the type named by an annotation or by the turbofish (parse::<i32>()), accepts only a clean trimmed number, and reports failure (InvalidDigit, Empty) through the value that expect unwraps-or-panics, properly handled in chapter 12.

Quiz time

Question #1

Three claims, true or false, one sentence of why:

a) const and an immutable let are interchangeable for a value that never changes. b) After let x = "5"; let x = 5; the program has mutated x. c) A function parameter for read-only text should usually be String.

Show solution

a) False: const additionally requires the value to be known at compile time, so runtime-but-frozen values (user input) can only be let. b) False: nothing was mutated; two variables existed, the second (a different type, which mut could never allow) now shadows the first. c) False: &str is the read-only text parameter, taking literals and lent Strings alike; String parameters are for functions that keep the text (chapter 8 business).

Question #2

What does this print?

fn main() {
    let width = 4;
    {
        let width = width * 2;
        println!("inner: {width}");
    }
    println!("outer: {width}");
    println!("[{:^6}]", width);
}
Show solution
inner: 8
outer: 4
[  4   ]

The block's shadow (computed from the outer value, then standing in front of it) vanishes with the block. Centering one character in six leaves five spaces, split two left, three right: the extra space goes right.

Question #3

This program compiles and works, but it contains three things this chapter taught you to fix. Find them and rewrite it:

fn shipping_label(name: String) {
    println!("Ship to: {name}");
}

fn main() {
    let mut input = String::new();
    println!("Customer name?");
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");

    let cleaned = input.trim();
    shipping_label(cleaned.to_string());

    println!("Insurance: {}", 25.0 * 0.012);
}
Show solution

The three fixes: shipping_label only reads the name, so its parameter should be &str (which also deletes the awkward .to_string() at the call site); the raw/cleaned pair is shadowing's exact use case, retiring the still-reachable untrimmed input; and 0.012 (and arguably 25.0) are magic numbers, meaningless to the next reader and unfindable at update time.

const INSURANCE_RATE: f64 = 0.012;
const ORDER_VALUE: f64 = 25.0;

fn shipping_label(name: &str) {
    println!("Ship to: {name}");
}

fn main() {
    let mut input = String::new();
    println!("Customer name?");
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");

    let input = input.trim();
    shipping_label(input);

    println!("Insurance: {}", ORDER_VALUE * INSURANCE_RATE);
}

Question #4

The chapter's program. Ask for two people's names and ages, then announce who's older, formatted exactly like the sample run (note the aligned table, two-wide age column). Decompose: a function that reads a name, a function that reads an age, and a function that takes both people and does the announcing (think about its parameter types). If the ages are equal, say so.

First person's name?
Ada
First person's age?
36
Second person's name?
Grace
Second person's age?
45
   Ada 36
 Grace 45
Grace is older than Ada.
Show solution
use std::io;

fn read_name(prompt: &str) -> String {
    println!("{prompt}");
    let mut name = String::new();
    io::stdin()
        .read_line(&mut name)
        .expect("failed to read input");
    name.trim().to_string()
}

fn read_age(prompt: &str) -> u32 {
    println!("{prompt}");
    let mut age = String::new();
    io::stdin()
        .read_line(&mut age)
        .expect("failed to read input");
    age.trim().parse().expect("that wasn't a whole number")
}

fn announce(name_a: &str, age_a: u32, name_b: &str, age_b: u32) {
    println!("{name_a:>6} {age_a:>2}");
    println!("{name_b:>6} {age_b:>2}");
    if age_a > age_b {
        println!("{name_a} is older than {name_b}.");
    } else if age_b > age_a {
        println!("{name_b} is older than {name_a}.");
    } else {
        println!("{name_a} and {name_b} are the same age.");
    }
}

fn main() {
    let first_name = read_name("First person's name?");
    let first_age = read_age("First person's age?");
    let second_name = read_name("Second person's name?");
    let second_age = read_age("Second person's age?");

    announce(&first_name, first_age, &second_name, second_age);
}

Design notes, chapter by chapter: read_name returns an owned String (the caller is keeping the text, so a view of the function's local input wouldn't survive; that intuition becomes a compiler-enforced law in chapter 9), while every parameter that reads text is a &str, lent with & at the call site. read_age is the 1.12 recipe in its lesson 5.6 form. The table comes from {:>6} and {:>2}, and the else if chain handles the tie the question demanded. (The near-duplicate read_name/read_age pair should itch slightly; generics, chapter 15, are the scratch.)

Question #5

In your Question #4 solution, could first_age be declared const FIRST_AGE: u32 instead? Why or why not?

Show solution

No. A const needs its value at compile time, and first_age doesn't exist until a user types it at runtime; the compiler would refuse with E0435 (attempt to use a non-constant value in a constant). Immutable let is exactly the right tool: frozen after creation, but created from the running world. That's the chapter's first lesson and its last word.

Next chapter: the operators you've been using on credit since chapter 1 (+, ==, %, and the gang) get their formal introductions, their precedence table, and their edge cases. Bring the floats; 4.5's == warning finally gets its proper tool.