Module: Ownership and Borrowing

References

Rust: Ownership and Borrowing - References

References are a core part of Rust's ownership system, enabling safe and efficient memory management without a garbage collector. They allow you to access data without taking ownership of it. This document will cover the details of references, including mutable and immutable references, borrowing rules, and dangling pointers.

What are References?

A reference is a way to access data owned by another variable. Think of it as a pointer, but with extra safety guarantees enforced by the Rust compiler.

Key characteristics of references:

  • Don't own the data: References point to data owned by another variable. When the owner goes out of scope, the data is dropped, and the reference becomes invalid.
  • Immutable by default: References are immutable by default, meaning you can't modify the data they point to.
  • Borrowing: Creating a reference is called "borrowing" because you're temporarily borrowing access to the data.
  • Lifetimes: References have lifetimes, which the compiler tracks to ensure they don't outlive the data they point to.

Creating References

You create a reference using the ampersand (&) operator.

let x = 5;
let ref_x = &x; // ref_x is an immutable reference to x

println!("x = {}, ref_x = {}", x, ref_x);

In this example:

  • x owns the integer value 5.
  • ref_x is an immutable reference to x. It doesn't own the 5; it just points to it.

Immutable References (&T)

Immutable references allow you to read the data they point to, but not modify it. You can have multiple immutable references to the same data simultaneously.

let x = 5;
let ref_x1 = &x;
let ref_x2 = &x;

println!("ref_x1 = {}, ref_x2 = {}", ref_x1, ref_x2);

This is perfectly valid because immutable references don't conflict with each other. They're all just reading the data.

Mutable References (&mut T)

Mutable references allow you to modify the data they point to. However, Rust enforces a strict rule: you can only have one mutable reference to a particular piece of data at a time. This prevents data races and ensures memory safety.

let mut y = 10; // y must be mutable to create a mutable reference
let ref_y = &mut y;

*ref_y = 20; // Dereference the mutable reference to modify the value

println!("y = {}", y); // Output: y = 20
  • &mut y creates a mutable reference to y.
  • *ref_y = 20 dereferences the reference (using the * operator) to access the underlying value and modify it.

Important: You must declare the original variable (y in this case) as mut to be able to create a mutable reference to it.

The Borrowing Rules

Rust's borrowing rules are the cornerstone of its memory safety. They are enforced at compile time.

  1. You can have either one mutable reference or any number of immutable references. You cannot have both simultaneously.
  2. References must always be valid. A reference cannot outlive the data it points to.

Let's illustrate these rules with examples:

Example 1: Valid - Multiple Immutable References

let z = 15;
let ref_z1 = &z;
let ref_z2 = &z;

println!("ref_z1 = {}, ref_z2 = {}", ref_z1, ref_z2);

This is valid because we have multiple immutable references.

Example 2: Valid - One Mutable Reference

let mut w = 25;
let ref_w = &mut w;
*ref_w = 35;

println!("w = {}", w);

This is valid because we have only one mutable reference.

Example 3: Invalid - Mutable and Immutable References Simultaneously

let mut a = 42;
let ref_a_imm = &a;
// let ref_a_mut = &mut a; // This line would cause a compile-time error!

println!("ref_a_imm = {}", ref_a_imm);

This is invalid because we're trying to create a mutable reference (ref_a_mut) while an immutable reference (ref_a_imm) already exists. The compiler will prevent this.

Example 4: Invalid - Mutable Reference After Immutable Reference

let mut b = 50;
let ref_b_imm = &b;
// let ref_b_mut = &mut b; // This line would cause a compile-time error!

println!("ref_b_imm = {}", ref_b_imm);

This is also invalid. Even if the immutable reference is still in scope, you can't create a mutable reference.

Dangling Pointers (and how Rust prevents them)

A dangling pointer is a reference that points to memory that has already been freed. Dangling pointers are a common source of bugs in languages like C and C++.

Rust prevents dangling pointers through its ownership and borrowing system. The compiler tracks the lifetimes of references and ensures that they never outlive the data they point to.

fn main() {
    let r;
    {
        let x = 5;
        r = &x; // r is a reference to x
    } // x goes out of scope here, and the memory is freed

    // println!("r: {}", r); // This would cause a compile-time error!
    // r is now a dangling pointer.  The compiler detects this and prevents you from using it.
}

In this example, x goes out of scope at the end of the inner block. Therefore, the reference r becomes invalid. The Rust compiler detects this and prevents you from dereferencing r because it would be a dangling pointer. This is a crucial safety feature.

Lifetimes (Brief Introduction)

While we won't delve deeply into lifetimes here, it's important to know they exist. Lifetimes are annotations that tell the Rust compiler how long a reference is valid. The compiler uses this information to enforce the borrowing rules and prevent dangling pointers. In many cases, the compiler can infer lifetimes automatically, so you don't need to write them explicitly. However, in more complex scenarios (like functions that return references), you may need to specify lifetimes.

Summary

References are a powerful and safe way to access data in Rust without taking ownership. Understanding the borrowing rules and how Rust prevents dangling pointers is essential for writing correct and efficient Rust code. By adhering to these rules, you can leverage the benefits of memory safety without the overhead of a garbage collector.