9.4References and functions
In the previous lesson (9.3), we fixed shout by changing its parameter from String to &str, and the compiler itself suggested the change. But the compiler can only spot ownership a function doesn't use. It can't tell you what a function you're about to write should ask for. That's a design decision, it comes up every single time you write fn, and it has a four-row answer you'll memorize by the end of this lesson.
The four choices
For any value a function needs, the parameter takes one of four shapes. In rough order of how often you'll reach for each:
fn area(width: f64, height: f64) -> f64 {
width * height
}
fn letter_count(text: &str) -> usize {
text.len()
}
fn add_excitement(text: &mut String) {
text.push_str("!!!");
}
fn decorated(text: String) -> String {
text + "!"
}
Copy types: take them by value. area's floats are lesson 8.5's table: numbers, bool, char, and friends live whole in their box, so passing one hands the function an honest, complete copy. No & buys you anything here; a reference to an i32 is the same size as the i32. When the type is Copy, write the plain type and move on.
Reading a non-Copy value: take &T. letter_count only looks, so it asks for a view. The caller keeps ownership, nothing is copied, and the signature promises the no-margin-notes rule from lesson 9.1. For text specifically the view type is &str rather than &String (lesson 5.4's rule; the final piece of why arrives in lesson 9.7). This is the workhorse row: when in doubt, borrow.
Modifying the caller's value: take &mut T. add_excitement changes a String the caller will keep using, so it asks for the writable loan, and the call site will say &mut in plain sight. You've been using the canonical example since chapter 1: read_line(&mut name) exists to fill your buffer. It modifies in place because the String is yours, you'll trim and parse it next, and you might reuse it for the next read. A writable loan is exactly the relationship.
Keeping the value: take ownership. decorated uses chapter 8's +, which consumes its left side: the function spends text to build its answer, so it asks, truthfully, to be given the value. Lesson 8.6 stated the rule from the caller's view, and it's worth restating from the designer's: ask for an owned String only when the function keeps or consumes the text. If it merely reads, taking ownership forces every caller into a clone or a funeral.
Key insight
A Rust signature is a contract about ownership, not just types. From &str / &mut String / String alone you know whether a function can change your value and whether you keep it afterward, before reading one line of its body. This is why experienced Rust programmers read an unfamiliar crate's function signatures the way you'd read a menu.
Out-parameters, and why Rust mostly skips them
There's a fifth pattern, beloved of older C and C++ code, that the &mut row makes possible in Rust: the out-parameter, a writable argument that exists only to carry a result back to the caller. Here's a function that builds an uppercase copy, both ways:
fn shouted_into(text: &str, out: &mut String) {
out.clear();
out.push_str(&text.to_uppercase());
}
fn shouted(text: &str) -> String {
text.to_uppercase()
}
fn main() {
let mut loud = String::new();
shouted_into("goal", &mut loud);
println!("{loud}");
let louder = shouted("goal so good");
println!("{louder}");
}GOAL
GOAL SO GOOD
Compare the call sites. The out-parameter version makes the caller pre-declare a mut variable, build an empty String it doesn't want, and thread &mut loud through the call before anything useful occurs. The return version is one line that reads like what it does. learncpp's lesson on this (12.13) reaches the same verdict for C++, and it's house advice here too: results come back in return values; when there are several, a tuple (lesson 4.11) carries them; &mut parameters are for functions whose purpose is modifying something the caller owns, not for smuggling outputs.
So what separates read_line and add_excitement (good &mut) from shouted_into (clumsy &mut)? Ask where the value's life is. add_excitement changes a String that existed before the call and matters after it: in-place modification, the real thing. shouted_into's out was born empty for the call and contains nothing but the result: that's a return value wearing a costume.
Best practice
Choose parameters by what the function does with the value: Copy types by value; &T to read; &mut T to modify something the caller keeps; owned T to keep or consume it. Return results as return values, not through &mut out-parameters.
Quiz time
Question #1
Choose the parameter type (u32, &str, &mut String, or String) for each described function, and say which row of the table decides it:
a) fn is_yes(answer: ???) -> bool returns whether the answer is "y" or "yes"
b) fn censor(comment: ???) replaces rude words in a comment the caller keeps using
c) fn frame(title: ???) -> String consumes the title, returning it wrapped in * characters via +
d) fn stars(count: ???) -> String returns a row of count stars
e) fn shorten(headline: ???) truncates a headline in place to 40 characters
Show solution
a) &str: reads only. b) &mut String: modifies a value the caller keeps. c) String: consumes its input (the + from chapter 8 spends its left side). d) u32: a Copy type, by value. e) &mut String: in-place modification again.
If you hesitated between (b)/(e) and (c): the question is whether the caller's variable should still hold the (modified) value afterward, or whether the function swallows it and hands back something new. Both are legitimate designs; the signature is where you commit to one.
Question #2
This function works, but one parameter is an out-parameter in costume. Redesign the signature (no body needed):
fn full_name(first: &str, last: &str, result: &mut String)Show solution
fn full_name(first: &str, last: &str) -> String
result exists only to carry the answer out, so it should be the return value. The inputs stay as &str views: reading names doesn't need ownership. Callers go from three lines and a hollow mut String down to let name = full_name(first, last);.
Question #3
A teammate proposes fn print_receipt(items: String) for a function that formats and prints a receipt, arguing "it compiles fine." What goes wrong for callers, and what's the fix?
Show solution
It compiles, but every call moves the items String into the function, where it dies after printing. Callers who need their data afterward are forced into print_receipt(items.clone()), paying for a copy of text that was only ever read. The signature demands ownership it doesn't use. Fix: fn print_receipt(items: &str), and callers lend with &items. (This is patient 3 from last lesson, caught at the design stage instead of the error-message stage, which is the cheaper place to catch it.)
One row of the table still has an untested edge. A function can take a reference, but can it return one? Sometimes, and the cases where it can't are the best stories in the chapter. Next lesson: dangling references, or, the bug Rust was built to kill.