Mastering Memory Management in Rust: Ownership and Variable Scope
Tags: rust, beginners, tutorial, programming
Published: 2024-03-18 18:30:00

Rust is well known for its memory safety and efficiency, largely due to its unique ownership system. In Rust, memory management revolves around ownership rules, which prevent common issues like memory leaks and data races. This article dives into the core concepts of ownership in Rust, exploring variable scope, ownership rules, data movement, and ownership in functions with clear examples.

## Variable Scope

A variable's scope in Rust defines where it is valid within the code. In Rust, variables are only accessible within the scope they're defined in. Once the scope ends, the variable goes out of scope, and its memory is automatically freed.

```rust
// Example of variable scope
{
    let name = String::from("hello rust!");
    // 'name' is valid within this code block only
}
// 'name' is no longer valid here
```
Here the variable `name` is only available inside the code block, i.e., between the curly braces `{}`. We cannot use the `name` variable outside the closing curly brace.

## Ownership Rules

Rust enforces ownership rules to ensure memory safety:
1. Each value has a single owner.
2. There can only be one owner at a time.
3. When the owner goes out of scope, the value is dropped.

## Data Move

When a value is assigned to another variable, ownership moves, preventing multiple ownership and ensuring memory safety.

```rust
fn main() {
    // rule no. 1 
    // String value has an owner variable `fruit1`
    let fruit1 = String::from("Banana");
    
    // rule no. 2
    // only one owner at a time
    // ownership moves to another variable `fruit2`
    let fruit2 = fruit1;
    
    // rule no. 3
    // error, out of scope, value is dropped
    println!("fruit1 = {}", fruit1);
    // cannot print variable fruit1 because ownership has moved
    
    // print value of fruit2 on the screen
    println!("fruit2 = {}", fruit2); // prints → Banana
}
```

Here, `fruit1` was the owner of the String `Banana`.

A `String` type stores data both on the `stack` and the `heap`. It means that when we bind a `String` to a variable `fruit1`, the memory representation looks like this:

**Stack(fruit1):**

| Name     | Value |
| -------- | ----- |
| ptr      | -->   |
| length   | 6     |
| capacity | 6     |

**Heap:**

| Index | Value |
| ----- | ----- |
| 0     | B     |
| 1     | a     |
| 2     | n     |
| 3     | a     |
| 4     | n     |
| 5     | a     |

A `String` type holds a pointer that points to the memory that holds the content of the string. The length and capacity of the string are stored in the stack.

Now, when we assign `fruit1` to `fruit2`, this is how the memory representation looks like:

**Stack(fruit2):**

| Name     | Value |
| -------- | ----- |
| ptr      | -->   |
| length   | 6     |
| capacity | 6     |

Rust will invalidate (drop) the first variable `fruit1`, and move the value to another variable `fruit2`. This way two variables cannot point to the same content. **Rule no. 2, there can only be one owner of the value at a time.**

> **Note:** The above concept is applicable for data types that don't have fixed sizes in memory and use the heap memory to store the contents.

## Data Copy

Primitive types like integers and floats, which have a fixed size, are copied instead of moved when assigned to another variable.

```rust
fn main() {
    let x = 11;
    
    // copies data from x to y
    // ownership rules are not applied here 
    let y = x;

    println!("x = {x}, y = {y}"); // prints x = 11, y = 11
}
```

Here, `x` variable can be used afterward, unlike a `move` without worrying about ownership, even though `x` is assigned to `y`.

This copying is possible because of the `Copy` trait available in primitive types in Rust. When we assign `x` to `y`, a copy of the data is made.

> **Note:** A `trait` is a way to define shared behavior in Rust. We will discuss the traits later.

## Ownership in Functions

When passing variables to functions, ownership can move or be copied, depending on the type of variable.

- Stack allocated types will copy the data when passed into a function.
- Heap allocated data types will move the ownership of the variable to the function.

**Example: Passing String to a function**
```rust
fn print_fruit(str: String) {
    // str comes into scope
    println!("str = {}", str);
} // str goes out of scope and is dropped, plus memory is freed

fn main() {
    // fruit comes into scope
    let fruit = String::from("apple");

    // ownership of fruit moves into the function
    print_fruit(fruit); // value moved here
    // prints → apple
    // fruit is moved to the function so is no longer available here

    println!("fruit = {}", fruit); // error
}
```

Here, the value of the `fruit` variable is moved into the function `print_fruit()` because `String` type uses heap memory.

**Example: Passing Integer to a function**
```rust
fn print_number(value: i32) {
    // value comes into scope
    println!("value = {}", value);
} // value goes out of scope and is dropped, plus memory is freed

fn main() {
    // number comes into scope
    let number = 10;

    // value of the number is copied into the function
    print_number(number);

    // number variable can be used here
    println!("number = {}", number);
}
```

Here, the value of the `number` variable is copied into the function `print_number()` because the `i32` (integer) type uses stack memory.

## Ownership table

A table summarizing types that implement the `Copy` trait and types that are typically heap-allocated.

- Types that implement `Copy` trait have their values copied when assigned to another variable, preventing ownership transfer.
- Heap-allocated types involve ownership transfer because they are dynamically allocated on the heap, and ownership needs to be managed properly.
- Whether a compound or custom type implements `Copy` depends on its internal structure.


| Type      | Implements `Copy` | Heap-Allocated | Ownership Transfer |
| ---------------- | ----------- | ----------------- | -------------- |
| Integer Types                  | Yes               | No             | No                 |
| Floating-Point Types           | Yes               | No             | No                 |
| Character Type                 | Yes               | No             | No                 |
| Boolean Type                   | Yes               | No             | No                 |
| Tuple (if elements are `Copy`) | Yes               | No             | No                 |
| Array (if elements are `Copy`) | Yes               | No             | No                 |
| Structs (if fields are `Copy`) | Yes               | No             | No                 |
| String                         | No                | Yes            | Yes                |
| Vec                            | No                | Yes            | Yes                |
| HashMap, HashSet               | No                | Yes            | Yes                |
| Custom Types                   | Depends           | Depends        | Depends            |


## Conclusion

Understanding ownership in Rust is crucial for writing safe and efficient code. By following ownership rules and mastering variable scope, developers can leverage Rust's memory management features to create robust and reliable software. Rust's ownership system, coupled with its powerful type system, ensures that memory-related errors are caught at compile time, resulting in more reliable software with fewer runtime surprises.