Rust track

Lesson 07 · Part 2: Ownership, Borrowing & Lifetimes

Borrowing & References

References, immutable and mutable borrows, and compile-time checks

Part 2: Ownership, Borrowing & Lifetimes4 min readFree lesson

As we saw in Part 6, if you pass a value like String to a function, the ownership moves entirely. It's inconvenient to get it back as a return value every time. Rust provides references to briefly “borrow” a value without transferring ownership. The borrowing rules you learn in this episode lead directly to safe concurrency.

Learning Objectives

  • Use the shared reference &T and the variable reference &mut T separately.
  • Memorize the borrowing rules (1 variable vs. N immutable) and apply them in the code.
  • Understand how dangling references are blocked at compile time.
  • Receives a reference to the function and allows the caller to continue writing the value.

Key concepts

This is a read-only borrow. Several can exist simultaneously at the same point in time.

fn len_of(s: &String) -> usize { s.len() }
let s = String::from("Rust");
let n = len_of(&s);   //Ownership Transfer

This is a modifiable lease. Only one can exist at any one time, and no other references (immutable or mutable) can be created while a mutable reference is alive.

fn append_excl(s: &mut String) { s.push('!'); }
let mut s = String::from("hello");
append_excl(&mut s);

Only one of the following two is allowed at the same time:

  • immutable reference multiple
  • variable reference one

Thanks to this rule, data races are blocked at the compilation stage. Thanks to non-lexical lifetimes (NLL) since Rust 1.31, references are automatically disabled after "last used", allowing new references to be created to the same variable.

The compiler blocks dangling references to values ​​that have already been dropped. If you try to return a reference to a local variable as is from a function, you will get a lifetime error — this is covered in more detail in Part 9.

See as example

//Part 7 Example 1: Shared References (
//
//Once a reference is received, ownership does not transfer, so the caller can continue to use the variable.

fn len_of(s: &String) -> usize {
    s.len()
} //Here, s is a borrowed reference and is not dropped.

fn main() {
    let s = String::from("Hello, Rust!");
    let n = len_of(&s); //Create reference: `
    println!("The length of \"{s}\" is {n}");

    //You can have multiple references at the same time (if they are immutable).
    let r1 = &s;
    let r2 = &s;
    println!("Two references to the same value: {r1} / {r2}");
}
//Part 7 Example 2: Variable Reference (
//
//To create a mutable reference, the source must also be `mut`,
//There can be **only** a mutable reference at any given time.

fn append_excl(s: &mut String) {
    s.push('!');
}

fn main() {
    let mut greeting = String::from("hello");
    append_excl(&mut greeting);
    append_excl(&mut greeting);
    println!("{greeting}");

    //Creating and using mutable references directly
    let r = &mut greeting;
    r.push_str(" Rust");
    println!("{r}");
    //From this point on, r will no longer be used, so you can create another reference.
    println!("(original) {greeting}");
}
//Part 7 Example 3: Borrowing Rules — At the same time
//(a) 1 mutable reference or
//(b) N immutable references
//Only one of the two is allowed.

fn main() {
    let mut v = vec![1, 2, 3];

    //Multiple immutable references are OK
    let a = &v;
    let b = &v;
    println!("a = {a:?}, b = {b:?}");
    //The above two references are last used here → Disabled thereafter

    //After disabled, new mutable references can be created. There is.
    let m = &mut v;
    m.push(4);
    println!("m = {m:?}");

    //The compiler also prevents dangling references:
    // fn dangle() -> &String {
    //     let s = String::from("oops");
    //
    // }
}

Common Mistakes

A: That's right. &str is both a slice of String and a string literal, making it more general-purpose. We'll cover slicing in detail in the next episode. First of all, in this episode, I used &String to learn the borrowing itself.

A. Check if the original variable is declared as mut. To create &mut s, you must declare it as variable like let mut s = ...;.

A. Not at the same time. After the first mutable reference is consumed (finally read), the NLL allows you to create a new mutable reference.

A. The reference itself is a simple pointer, so it behaves like a small Copy type. However, the owner of the “value pointed to by the reference” is still the original variable.

Cleanup

  • &T is a read-only borrow, and &mut T is a modifiable borrow.
  • 1 mutable or N immutable at any one time — no simultaneous allowed.
  • A function that receives a reference allows the caller’s value to be used as is.
  • The compiler automatically blocks dangling references.

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/borrowing/Cargo.tomlTOML
[package]
name = "borrowing"
version = "0.1.0"
edition = "2021"
default-run = "borrowing"

[dependencies]
src/borrowing/src/main.rsRust
fn main() {
    println!("This crate has three running examples.");
    println!();
    println!("  cargo run --bin ex01_share");
    println!("  cargo run --bin ex02_mut");
    println!("  cargo run --bin ex03_rules");
}

Assignment

  • File name: homework_01/src/main.rs
  • Key concepts: Shared references &str / &String, split_whitespace

Requirements

  • Define the function word_count(s: &str) -> usize to return the number of words divided by spaces.
  • Create let text = String::from("hello rust borrowing checker"); from main Pass a reference to a function.
  • Even after the call, text is printed again, showing that ownership has not moved.

Expected output

text = hello rust borrowing checker
word count = 4

Hint

  • s.split_whitespace().count().

  • Pass &str to &text or text.as_str().

  • File name: homework_02/src/main.rs

  • Key Concept: Variable Reference &mut Vec<i32>

Requirements

  • Define the function double_all(v: &mut Vec<i32>) to double all elements.
  • Create let mut nums = vec![1, 2, 3, 4]; from main and output before and after the call.

Expected output

: [1, 2, 3, 4] After calling
: [2, 4, 6, 8]

Hint

  • for n in v.iter_mut() { *n *= 2; }
  • Pass the variable reference to &mut nums.

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:
//- `
//- `

fn word_count(s: &str) -> usize {
    s.split_whitespace().count()
}

fn main() {
    let text = String::from("hello rust borrowing checker");
    let n = word_count(&text); //No transfer of ownership
    println!("text = {text}");
    println!("number of words = {n}");
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key points:
//- `
//- Internally, obtain a mutable reference to each element with `iter_mut()` and modify it with `*n *= 2;`.

fn double_all(v: &mut Vec<i32>) {
    for n in v.iter_mut() {
        *n *= 2;
    }
}

fn main() {
    let mut nums = vec![1, 2, 3, 4];
    println!("Before calling: {:?}", nums);
    double_all(&mut nums);
    println!("After calling: {:?}", nums);
}

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