root@coding-prodigies:~# โ–Š
// lesson 2 of 12 ยท 16 min

Ownership and borrowing, revisited

โš‘ Report an issue with this lesson
fn 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

Exercise: Complete print_length(s: &String) so it prints s.len(). In main, call it with a borrowed reference to name, then print name again afterward to prove the borrow ended and name is still valid.
Expected output:
3
Ada
rust
Output

      
    

Run your code and get it working before marking this lesson complete.

// free preview

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