Skip to content
Ownership and Borrowing
step 1/8

Reading — step 1 of 8

Learn

~4 min readFunctions and Ownership

Ownership is Rust's central idea. It's the reason you can write fast code without a garbage collector AND without segfaults, double-frees, use-after-free, or data races. The whole point of Rust is to enforce these guarantees at COMPILE TIME so they can't bite you in production.

Understanding ownership is what unlocks Rust. Once it clicks, the borrow checker stops feeling like an enemy and starts feeling like a co-pilot.

The three rules

  1. Every value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (memory freed).
  3. Ownership can be MOVED to a new owner — but the old owner can no longer use it.
let s1 = String::from("hello");   // s1 owns the heap string
let s2 = s1;                       // ownership MOVES to s2
// println!("{}", s1);             // ✗ compile error — s1 was moved
println!("{}", s2);                 // ✓ s2 is the new owner
// at end of scope, s2 is dropped → string freed exactly once

The move rule prevents double-frees. Both s1 and s2 can't free the same memory because s1 is no longer valid.

Move vs Copy — when assignment doesn't move

Simple types stored entirely on the stack are Copy. Assignment makes a cheap copy instead of moving:

let x = 5;
let y = x;          // x is COPIED — both still usable
println!("{} {}", x, y);   // ✓ 5 5

Copy types: integers, floats, booleans, char, tuples of Copy types, fixed-size arrays of Copy types.

Non-Copy types (move semantics): String, Vec, Box, HashMap, anything owning heap data.

To duplicate a non-Copy value, call .clone():

let s1 = String::from("hi");
let s2 = s1.clone();    // independent copy
println!("{} {}", s1, s2);    // both valid

Functions take ownership too

fn consume(s: String) {
    println!("{}", s);
}    // s dropped here

fn main() {
    let name = String::from("Alice");
    consume(name);                      // moves name into consume
    // println!("{}", name);             // ✗ name was moved
}

To USE a value in a function without taking ownership, borrow it:

Borrowing — & and &mut

fn read(s: &String) {       // shared (read-only) borrow
    println!("{}", s);
}

fn append(s: &mut String) {  // mutable borrow
    s.push_str("!");
}

fn main() {
    let mut name = String::from("Alice");
    read(&name);                  // borrow shared
    append(&mut name);             // borrow mutably
    println!("{}", name);          // "Alice!" — name is still owned by main
}

&T is a shared reference (read-only). &mut T is a mutable reference (write access).

The borrow checker's two rules

At any given moment, you can have EITHER:

  • Many shared (&) borrows of a value, OR
  • Exactly ONE mutable (&mut) borrow of a value.

Never both. This rule prevents data races at compile time.

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;             // ✓ multiple shared borrows OK
println!("{} {}", r1, r2);

let r3 = &mut s;          // ✗ wait — r1 and r2 still 'in use'?

In modern Rust (NLL — non-lexical lifetimes), the compiler tracks LAST use, not just scope. So r1, r2 are "done" at their last use. The above actually compiles in the rearranged form:

let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);   // r1, r2 last used here
let r3 = &mut s;             // ✓ now allowed
r3.push_str("!");

Why &str and not &String?

A &str is a SLICE — a borrow of a string's bytes. It works for both String and string literals:

fn greet(name: &str) {        // accepts both
    println!("Hi, {}", name);
}

greet("Alice");                  // string literal — &'static str
let s = String::from("Bob");
greet(&s);                       // String → &str via deref coercion

Idiomatic Rust functions take &str for read-only string params, NOT &String.

Lifetimes — a preview

Every reference has a lifetime — how long the borrow is valid. Most are inferred ("elided"), but sometimes you need to write them:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'a is a lifetime parameter — a name for "some lifetime." The function says: "the returned reference lives at LEAST as long as the SHORTER of x and y."

Deep dive in Rust Intermediate.

Why this matters

C and C++ have all the same rules — but they're enforced by the programmer (and bugs slip through). Rust enforces them at compile time. Whole categories of CVEs (use-after-free, double-free, iterator invalidation, data races) become impossible in safe Rust.

Common mistakes

  • Using a value after move: let s2 = s1; println!("{}", s1) — error. Either use s2 or don't move.
  • Mixing mutable and shared borrows: &mut s while &s is active — error. Rearrange so usages don't overlap.
  • Cloning to escape the borrow checker when borrowing would do — wasteful. Try fixing the borrow first.
  • Returning a reference to a local: the local goes out of scope — dangling reference. Compile error in safe Rust.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…