Tuesday, September 3, 2024

Rust Tutorial: Ownership and Borrowing on Memory Management


Rust's ownership and borrowing system is a core concept for its robust memory management. In this blog post, we'll explore references in detail, building upon the fundamental principles of Rust ownership.

Understanding Ownership in Rust

Rust's ownership system is unique in that every piece of data in memory has exactly one owner, a variable that's responsible for its allocation and deallocation. Data can be stored in two places: the stack or the heap.

  • Stack: Variables with fixed sizes, like integers, floats, or booleans, are stored on the stack. Stack allocation is incredibly efficient, as it's direct and fast.

  • Heap: Variables with variable sizes, such as strings or vectors, are stored on the heap. Heap allocation is more flexible but slower due to the need to search for the right location in memory.

Example:

let x = 2; // Variable 'x' is stored on the stack

let y = x; // The value of 'x' is copied into 'y' (cheap copy on stack)

println!("x: {x}"); // Output: x: 2

println!("y: {y}"); // Output: y: 2
    

The distinction between the stack and heap becomes crucial when dealing with data on the heap.

Example:

let a = "hello".to_string(); // Variable 'a' is stored on the heap

let b = a; // 'b' becomes the new owner of the data, 'a' becomes invalid

println!("{a}"); // Error: 'a' is invalid
    

As 'b' becomes the new owner of the data, 'a' loses access to it. This prevents uncontrolled simultaneous access to the same data.

References and Borrowing

Sometimes, we want to access data owned by another variable without taking ownership. This is where borrowing comes in. Borrowing is done by creating special pointers called references that point to the memory address of the owner, enabling access to the data without ownership transfer.

Example:

let a = "hello".to_string();

let b = &a; // 'b' is a reference to 'a'

println!("a: {a}"); // Output: a: hello

println!("b: {b}"); // Output: b: hello
    

The Rules of Borrowing

There are specific rules that govern the creation and use of references:

  1. References are always valid: A reference is only valid as long as its owner is valid. If the owner is moved or dropped, all references pointing to it become invalid.

  2. Mutability: References can be immutable or mutable. There are two types of references:

    • Immutable References: These references can only read data. They cannot modify the value they point to.

    • Mutable References: These references can modify the value of the data they point to.

Example:

let mut v = vec![0, 1, 2]; // 'v' is mutable

// Mutable Reference:
let u = &mut v; 
u.push(3); // Modifying the content of 'v' through 'u'

println!("vector v: {v:?}"); // Output: vector v: [0, 1, 2, 3]
    
  1. Only one mutable reference at a time: We can only have one mutable reference to a piece of data. Other references must be immutable.

Example:

let mut a = vec![0, 1, 2];

let b = &a; // Immutable Reference

let c = &a; // Immutable Reference

let d = &mut a; // Mutable Reference

d.push(3); // Modifying 'a' through 'd'

println!("{c}"); // Error: 'c' is invalid because there is a mutable reference 'd'

println!("{d:?}"); // Output: [0, 1, 2, 3]
    

Introducing Lifetimes

References cannot outlive their owners.

Example:

let s;

{
  let t = 5;
  s = &t; // 's' points to 't'
}

println!("{s}"); // Error: 't' is no longer valid
    

When 't' goes out of scope, 's' becomes invalid because it points to memory that has been freed. To prevent this, Rust uses the concept of lifetimes.

Lifetimes: Labels assigned to parts of code where variables are defined.

Example:

fn main() {
  // LIFETIMES

  //---------------------- 'a
  let s;               //|
                       //|
  {                    //|
  //---------------'b    |
    let t = 5;  //|      |
                //|      |
    s = &t      //|      |
  //---------------'b    |
  }                    //|
  //---------------------|'a
}
    

In this example, 's' has a lifetime 'a' which is longer than 't' which has a lifetime 'b'. When 't' goes out of scope, 's' becomes invalid because it points to freed memory.

Lifetimes in Functions

In functions, lifetimes are often necessary to indicate how the lifetime of the returned value is related to the lifetimes of the function's parameters.

Example:

fn example_1() -> &i32 { // Error: 'x' goes out of scope
  let x = 2;
  &x
}

fn example_2(x: &i32) -> &i32 { // Valid: 'x' remains valid
  &x
}

fn example_3<'a>(x: &'a i32, y: &'a i32) -> &'a i32 { // Valid: 'x' or 'y' remains valid
  &x
}
    

Conclusion

References in Rust are a key concept for understanding manual memory management. By grasping the ownership system and borrowing rules, you can write code that is efficient, safe, and free from memory errors.

0 comments:

Post a Comment