<< Home

Chapter 3: Ownership

Ownership is Rust’s core model for memory safety. Each value has a single owner; when the owner goes out of scope the value is dropped. The compiler enforces this at compile time, so you get safety without a garbage collector. This chapter explains how values move, when they copy, how borrowing works, why mutability matters to borrows, how slices fit into the picture, and how to read lifetime annotations in plain terms.

Moves and copies

When you assign or pass a value, Rust either moves it or copies it. A move transfers ownership to a new variable or parameter; a copy duplicates the bits so both variables remain valid. Types that implement the Copy trait perform a bitwise copy; most scalar types do. Heap-owning types like String and Vec<T> move by default.

⚠️ A scalar type is a single, simple value such as an i32, u8, f64, char or bool. These occupy a fixed amount of memory and have no heap allocation, which is why they can be duplicated safely with a straightforward bitwise copy.

Copy types

Numbers and other small scalars implement Copy. After assignment both bindings are usable, because the value was duplicated.

let a: i32 = 10;
let b = a;             // i32 implements Copy
println!("{a}, {b}");  // both fine
⚠️ It is often clearer to declare the type for important values, especially when teaching or reading unfamiliar code. Rust is able to infer many types on its own, and simple literals such as 10 default to i32, but explicit annotations help readers see your intent and help the compiler catch mistakes earlier.

Other common Copy types include bool, char, and tuples that contain only Copy members such as (i32, i32). If a tuple contains a non-Copy member then the tuple is not Copy.

Move semantics for heap-owned values

Types that own heap memory move, which prevents double frees and data races. After a move the previous binding becomes invalid.

let s1 = String::from("hello");
let s2 = s1;  // move: s1 is no longer valid
println!("{s2}");
⚠️ Rust moves ownership for types like String because they manage heap memory. A move prevents two variables from thinking they both own the same allocation. Other languages usually hide this detail and rely on a garbage collector, but Rust treats ownership as an explicit rule so memory is released safely and predictably when the owner goes out of scope.

Passing such a value to a function also moves it unless you borrow it. Returning a value moves ownership back to the caller unless you borrow it instead.

💡 If a type implements Clone, you can request an explicit deep copy with .clone(). This may allocate; use it when you really need a duplicate.

Destructuring and partial moves

Pattern matching can move some fields out of a struct while leaving others borrowed or moved differently. After a partial move, only the moved fields are unavailable; the rest can still be used according to the pattern.

struct User { name: String, id: u32 }

let u = User { name: String::from("Ada"), id: 7 };
let User { name, id } = u;  // name moves, id copies
println!("{name} #{id}");
// u is no longer fully usable here; its name field was moved
⚠️ The id field copies because u32 is a scalar that fits entirely on the stack, so duplicating it is trivial. The name field moves because a String owns heap memory and cannot be safely duplicated without an explicit clone. This contrast is a direct result of Rust’s rule that only Copy types duplicate automatically; everything else transfers ownership instead.

Borrowing and references

Borrowing lets you use a value without taking ownership. A reference points to a value owned by something else. You create a reference with &value for an immutable borrow or &mut value for a mutable borrow. The compiler checks that references never outlive their owners and never violate aliasing rules.

⚠️ You do not return a borrow in a manual way. Instead the compiler ends a borrow when the reference goes out of use. At that point ownership is simply back with the original variable because it never left in the first place. The reference was only a temporary permission slip, and the compiler tracks exactly how long that permission is valid. This keeps borrowing lightweight and predictable.

Immutable &T borrows

Any number of immutable borrows can coexist, which supports safe shared reads. The underlying value cannot be modified through an immutable borrow.

let s = String::from("hello");
let r1: &String = &s;
let r2 = &s;
println!("{r1} {r2}");  // shared read access

Immutable borrows are the default choice when you only need to read. They keep code simple and allow more flexible usage.

Mutable &mut T borrows

A mutable borrow grants exclusive write access for a limited region of code. There can be only one active mutable reference to a value at a time, and no immutable references may be active to that same value while the mutable borrow is alive.

let mut s = String::from("hi");
let r = &mut s;  // exclusive mutable borrow
r.push_str(" there");
println!("{r}");
⚠️ A mutable borrow and an immutable borrow to the same value cannot overlap in time; the compiler tracks these lifetimes precisely and rejects code that would allow aliasing with mutation.

Borrow scopes end at last use

Borrow scopes often end earlier than the enclosing block; they end at the last use of the reference. This can help you introduce a mutable borrow after immutable borrows in the same block.

let mut s = String::from("abc");
let r1 = &s;
println!("{r1}");  // r1 last used here
let r2 = &mut s;   // now allowed; r1's scope ended
r2.push('d');

Mutable vs immutable borrows

Choosing between &T and &mut T affects what other borrows can exist at the same time. Prefer immutable borrows for reading; upgrade to mutable borrows only for the smallest section of code that needs to change the value. This leads to clearer intent and fewer conflicts.

Aliasing rules in practice

The rules can be summarized as: many readers or one writer. This avoids data races and is enforced at compile time without runtime overhead.

let mut v = vec![1, 2, 3];

let a = &v;      // shared borrow
let b = &v;      // another shared borrow
println!("{:?} {:?}", a, b);

// a and b are no longer used after this point; now we can borrow mutably
let m = &mut v;  // exclusive mutable borrow
m.push(4);

Interior mutability

Some types allow mutation through an immutable reference by enforcing safety with runtime checks. This pattern is called interior mutability; Cell<T> and RefCell<T> are common tools. They are advanced; you will meet them later when covering smart pointers.

💡 Keep mutable borrows narrow. Create them as late as possible and drop them as soon as possible to reduce contention with other borrows.

Slices and string slices

A slice is a view into a sequence; it borrows a contiguous range of elements without owning them. Slices are written with range syntax on collections like arrays, vectors, and strings. A slice reference has the type &[T] for elements of type T or &str for string slices.

Array and vector slices &[T]

Use ranges to take slices. The resulting slice borrows the original collection, so the collection must outlive the slice.

let nums = vec![10, 20, 30, 40, 50];
let mid: &[i32] = &nums[1..4];  // 20, 30, 40
println!("{:?}", mid);

Ranges support several forms: start..end excludes the end; start..=end includes the end; ..end starts at zero; start.. continues to the end; .. means the whole range.

String slices &str

String owns UTF-8 bytes on the heap; &str is a borrowed slice of UTF-8. Because characters may be multiple bytes, slicing must align on byte boundaries for valid UTF-8. Indexing by position is not allowed; you slice by byte ranges carefully.

let s = String::from("résumé");
let first = &s[..2];  // "ré" in bytes; careful with UTF-8 boundaries
println!("{first}");
⚠️ Slicing in the middle of a multi-byte character panics at runtime. Use iterator methods like chars() when working in character units.

Slices borrow; they do not own

Because slices borrow, their lifetime is tied to the owner. You cannot return a slice to data that will be dropped; you must ensure the owner lives long enough.

fn first_two(v: &[i32]) -> &[i32] {
  &v[..2]
}

This function returns a slice borrowed from its input; the caller must keep the original slice or collection alive for as long as it uses the returned slice.

Lifetimes in plain terms

Lifetimes describe how long references are valid. The compiler uses lifetime analysis to guarantee that no reference outlives the data it points to. In many cases lifetimes are inferred; when they cannot be inferred you add annotations that connect inputs to outputs.

Reading lifetime annotations like <'a>

When you see <'a> on a function or type, read it as: there is a named lifetime called 'a. An annotation like -> &'a str means the returned reference is valid for at most the lifetime 'a. An input such as x: &'a str means x must live at least as long as 'a. Annotations connect these together so the compiler knows which inputs the output depends on.

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

Here, the returned &str is guaranteed not to outlive either input; it lives no longer than the shorter of their lifetimes. The annotation does not change lifetimes; it documents and constrains their relationship.

Borrowed returns vs owned returns

If a function creates new data inside itself, it should usually return an owned value so the caller can use it freely. Returning a borrowed reference to data created inside the function would reference data that ends when the function returns; that would be invalid.

fn make_string() -> String {
  String::from("made here")
}

Owned returns transfer ownership to the caller; borrowed returns tie the result to an input parameter’s lifetime. Choose based on whether the result depends on caller data or on newly created data.

Structs with references

A struct that holds a reference must declare a lifetime parameter and annotate reference fields so the compiler can verify that any instance does not outlive the data it borrows.

struct Excerpt<'a> {
  text: &'a str
}

fn highlight<'a>(text: &'a str) -> Excerpt<'a> {
  Excerpt { text }
}

The instance of Excerpt<'a> cannot outlive the string slice it references. The annotations tell the compiler exactly that relationship.

💡 When you are unsure about lifetimes, try to return owned data or restructure code so borrows do not need to escape their immediate scope; this often simplifies the design.

Throughout the rest of the book you will see these ownership rules at work. Whenever you encounter braces with omitted details such as { … }, remember that owners, borrows, and lifetimes govern when values are valid and who may access them at that moment.