DEV Community

Dipankar Paul
Dipankar Paul

Posted on

Mastering Memory Management in Rust: Ownership and Variable Scope

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.

// Example of variable scope
{
    let name = String::from("hello rust!");
    // 'name' is valid within this code block only
}
// 'name' is no longer valid here
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)