Reading — step 1 of 7
Learn
Borrowing — & and &mut
Rust's ownership system is three rules:
- Every value has exactly one owner.
- When the owner goes out of scope, the value is dropped (freed) — no garbage collector needed.
- Instead of transferring ownership, you can borrow:
&Tto read,&mut Tto modify.
The owner is whoever is responsible for freeing the memory. A borrow is a temporary permission slip: "you may look" (&) or "you may modify — and nobody else may even look while you do" (&mut).
Moves: assignment transfers ownership
For heap-owning types like String and Vec, assignment doesn't copy — it moves:
let s1 = String::from("hi");
let s2 = s1; // ownership moves to s2
println!("{}", s1); // trap: s1 is gone
error[E0382]: borrow of moved value: `s1`
Why so strict? If s1 and s2 both owned the same heap buffer, both would free it when dropped — a double free. Rust invalidates s1 at compile time instead.
- Need a second independent copy?
.clone()— an explicit, visible heap allocation. - Small stack-only types (
i32,f64,bool,char, and tuples/arrays of such types) areCopy: assignment copies, nothing moves. Your own structs are notCopyautomatically — you must opt in with#[derive(Copy, Clone)], and only if every field is itselfCopy.
Borrowing: lend, don't move
Usually a function just needs to use a value, not own it:
fn print_len(s: &String) { // shared borrow — read-only
println!("{}", s.len());
}
fn append_world(s: &mut String) { // exclusive borrow — may mutate
s.push_str(", world");
}
fn main() {
let mut name = String::from("hello");
print_len(&name); // lend read access
append_world(&mut name); // lend write access
println!("{}", name); // hello, world — main still owns it
}
append_world changes the caller's String in place. No return value needed: the mutation happens through the &mut reference.
The XOR rule
At any moment a value may have either any number of &T or exactly one &mut T — never both:
let mut v = vec![1, 2, 3];
let first = &v[0]; // shared borrow still alive...
v.push(4); // ...while we mutate
println!("{}", first);
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
This looks pedantic until you see why: push can reallocate the vector's buffer, leaving first pointing at freed memory. The XOR rule makes that use-after-free unrepresentable — and the same rule, applied across threads, is why safe Rust has no data races.
One more trap: to lend &mut, the binding itself must be mut. Writing let s = ...; and then f(&mut s) fails with error[E0596]: cannot borrow s as mutable, as it is not declared as mutable.
Your exercise
Define fn append_excl(s: &mut String) that appends ! — one line: s.push('!'); (or s.push_str("!")). Then uncomment the call in main. The starter already reads one line, trims the trailing newline, and prints the string.
TEST 1 feeds hello and expects exactly:
hello!
Mistakes the grader catches:
- Leaving the call commented out — compiles fine, prints
hello, fails (expectedhello!). - Taking
s: Stringby value — the call site passes&mut s, so you geterror[E0308]: mismatched types. - Taking
s: &String— the call compiles (&mutcoerces to&), but the mutation inside fails:error[E0596]: cannot borrow *s as mutable, as it is behind a & reference. Only&mut Stringgrants write access. - Returning a new
String— the function returns(). Mutate through the reference; the caller'sschanges in place.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…