Rust track

Lesson 06 · Part 2: Ownership, Borrowing & Lifetimes

Ownership

Ownership rules, moves and the Copy/Clone traits

Part 2: Ownership, Borrowing & Lifetimes5 min readFree lesson

The secret to Rust's ability to ensure memory safety without a garbage collector is ownership. Every value has exactly one "owner", and the value is freed immediately when the owner goes out of scope. If you learn the three rules in this section, you will be much better at reading all compiler errors in the future.

Learning Objectives

  • Memorize the three rules of ownership and identify them in code.
  • Know the difference between stack and heap, and know which types are stored where.
  • Distinguish between Copy types and non-Copy types (e.g. String).
  • When a value is passed to a function, it tracks how ownership moves.

Key concepts

  1. Every value in Rust has a variable called owner.
  2. There is only an owner at any one time.
  3. When the owner goes out of scope, the value is droped immediately.

With just these three rules, memory bugs like use-after-free and double-free are blocked at the compilation stage.

  • Stack: A value with a fixed size (i32, bool, char, fixed tuple/array, etc.). Fast and automatically released.
  • Heap: Values ​​with variable or dynamic size (String, Vec<T>, Box<T>). There are allocation and release costs.

String places the actual character data on the heap, and only the metadata of (pointer, length, capacity) is placed on the stack.

Non-Copy types such as String ownership transfers in assignments and function calls. The moved variable cannot be used from that point on.

let s1 = String::from("hello");
let s2 = s1;     //Move s1 → s2
//println!("{s1}"); // ❌ No longer valid

The scalar types (i32, f64, bool, char, and their own tuple/array) implement the Copy trait. When assigning or transferring, only bit copy occurs and the original can be used as is.

Type example move/copy Reason
i32, f64, bool, char copy Fixed size/stack storage
(i32, i32), [u8; 4] copy All elements Copy
String, Vec<T>, Box<T> move owning heap resources

When you pass a value as an argument to a function, the parameter becomes the new owner. If the caller wants to continue using the same value, (a) return it as a return value or (b) use reference in the next section.

See as example

//Part 6 Example 1: Moving ownership of String (move)
//
//For types that have data on the heap (String, Vec, Box, etc.), the
//value is "moved" during assignment or function calls. The moved variable is no longer available.

fn main() {
    let s1 = String::from("hello"); //s1 is the owner of this heap data
    let s2 = s1;                    //Ownership is moved to s2
    //println!("{s1}");            // ❌ Compilation error: s1 is no longer valid
    println!("s2 = {s2}");

    let s3 = String::from("Rust");
    take(s3);                       //When passed to a function, ownership moves to the argument
    //println!("{s3}");            // ❌ s3 is also no longer valid

    println!("When the scope ends, the owner is dropped and the memory is released.");
}

fn take(s: String) {
    println!("Value received by take: {s}");
    //When the function ends, s is dropped.
}
//Part 6 Example 2: A type that implements the Copy trait — operates as a copy
//
//A scalar type (i32, f64, bool, char, ...) and a tuple consisting of only them is `Copy`.
//Since bit copying occurs during assignment and transfer, it does not look like “moving”.

fn main() {
    let a = 10;
    let b = a;        //The bit of a is moved to b. Copy
    println!("a = {a}, b = {b}"); //a is also still available

    let p = (1, 2);   //(i32, i32) also after calling Copy
    let q = p;
    println!("p = {:?}, q = {:?}", p, q);

    show(a);
    println!("show a = {a}");
}

fn show(n: i32) {
    println!("show: {n}");
}
//Part 6 Example 3: Reclaiming ownership with return value When you pass a String to the
//
//function, ownership moves. If you want to write it again, you can return it as the return value of
//, or use a "reference" that you will learn in the next part.

fn main() {
    let s = String::from("hello");
    let s = grow(s); //Pass s and bundle the new returned String back into s (shadowing)
    println!("Result: {s}");
}

fn grow(mut s: String) -> String {
    s.push_str(", world!");
    s //Return ownership to the caller
}

Common Mistakes

A. There are two ways. (1) Make the function return the same String again, or (2) Lend it as a reference (&String or &str). Usually (2) is natural — we'll learn in the next section.

A. The compiler determines whether the Copy trait is implemented in the type. i32 is not Copy or String. It is the same syntax, but operates differently depending on the type.

A. Explicitly calling s1.clone() creates a new String that replicates the heap data as well. It's expensive, so use it only when you really need it.

A. This is the moment when the variable leaves the scope { ... }. For types that implement the Drop trait (String, Vec, Box, etc.), the resource release code is automatically called at this time. You can also explicitly drop it early with the drop(value) function.

Cleanup

  • Every value has only one owner, and is dropped when the owner goes out of scope.
  • Heap resource types (String, Vec, Box) are moved when assigned or passed.
  • The scalar type implements Copy and operates as a copy.
  • To rewrite the value passed to the function, return it or use the reference in the next section.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/ownership/Cargo.tomlTOML
[package]
name = "ownership"
version = "0.1.0"
edition = "2021"
default-run = "ownership"

[dependencies]
src/ownership/src/main.rsRust
fn main() {
    println!("This crate has three running examples.");
    println!();
    println!("  cargo run --bin ex01_move");
    println!("  cargo run --bin ex02_copy");
    println!("  cargo run --bin ex03_return");
}

Assignment

  • File name: homework_01/src/main.rs
  • Key concept: How to avoid transfer of ownership of String (return or clone)

Requirements

  • Create let s = String::from("Rust");.
  • Call the len_of function to receive the length.
  • Even after the call, s itself must be able to be used again.
  • Prints Rust is 4. in the last line.

Expected output

Rust is 4.

Hint

  • Two solutions are possible.

  • (A) Make the function return a (String, usize) tuple and return a String.

  • (B) Copy it as s.clone() and pass it to the function. (It costs money)

  • The correct answer uses method (A) — the most natural way to solve it without the reference you will learn in the next part.

  • File name: homework_02/src/main.rs

  • Core concept: Behavior of Copy trait type

Requirements

  • Create variable let n = 42;.
  • Define function print_squared(n: i32) to output n * n.
  • After calling print_squared(n);, main is displayed again as n.

Expected output

Sq: 1764
Original: 42

Hint

  • i32 can continue to be used as the original even after passing it to the Copy function.
  • Write like println!("Square: {}", n * n);.

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key points:
//- When a String is passed to a function, ownership moves.
//- If a tuple (original, result) is returned together, the caller can use it again.
//- Refer to what you will learn in the next section (

fn len_of(s: String) -> (String, usize) {
    let len = s.len();
    (s, len) //Returns ownership to the caller while also returning the length.
}

fn main() {
    let s = String::from("Rust");
    let (s, len) = len_of(s); //Shadowing re-binds to the same name
    println!("The length of {s} is {len}.");
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key points:
//- i32 is a Copy type, so the original is not invalidated when passed to the function.
//- The value received within the function is actually a "copy".

fn print_squared(n: i32) {
    println!("Squared: {}", n * n);
}

fn main() {
    let n = 42;
    print_squared(n);
    println!("Original: {n}"); //Still usable
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now