Ownership and borrowing, revisited
โ Report an issue with this lessonfn print_length(s: &String) {
println!("{}", s.len());
}
fn main() {
let name = String::from("Ada");
print_length(&name); // borrow, doesn't take ownership
println!("{}", name); // still valid here
}
The rule that makes Rust memory-safe without a garbage collector: at any point, you have either one mutable reference, or any number of immutable references -- never both at once. The compiler enforces this at compile time, which is why Rust code that compiles rarely segfaults or data-races.
It helps to be precise about what "ownership" actually means. Every
value in Rust has exactly one owner at a time, and when that owner goes
out of scope, the value is dropped -- its destructor (the
Drop implementation, if it has one) runs automatically.
Assignment moves ownership by default for types that don't implement
Copy:
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2 -- s1 is no longer valid
println!("{}", s2); // fine
// println!("{}", s1); // compile error: value borrowed after move
Simple stack-only types like i32, bool, and
char implement the Copy trait, so assignment
copies the bits instead of moving -- both variables stay valid. That's
why let x = 5; let y = x; println!("{}", x); compiles fine
while the equivalent with a String doesn't: a
String owns heap data, and Rust doesn't want two owners
both believing they're responsible for freeing the same allocation.
Borrowing lets you use a value without taking ownership of it, which
is why print_length above can accept
&String instead of String -- the caller keeps
ownership, and the borrow ends automatically when the reference goes
out of scope. The compiler tracks borrow lifetimes at the granularity
of "when is this reference last used," not just lexical scope (this is
called non-lexical lifetimes), so this compiles even though
r1's scope textually overlaps the mutable borrow:
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1's last use is here
let r2 = &mut s; // fine -- r1 is no longer "alive" by this point
r2.push_str(" world");
A common early mistake is fighting the borrow checker by cloning
everything to make errors go away (value.clone()
everywhere). That works, but it defeats the point -- you're paying for
copies to route around a design that would compile for free with the
right borrow structure. As a rule of thumb: pass
&T when a function only needs to read, &mut T
when it needs to modify in place, and only pass owned T
when the function genuinely needs to consume or store the value beyond
the call.
Try it yourself
3 Ada
Run your code and get it working before marking this lesson complete.
9 more lessons โ including Project: build a concurrent word counter โ and a final exam plus a certificate are waiting.
Unlock the full course โ $99.99