Rust Ownership 101

Rust Ownership 101

One of the most common pitfalls that Rust beginners face is the Ownership concept. It is a set of crucial yet simple rules that makes Rust so popular. However, it is quite divergent from how other languages handle similar situations.

In this article, we’ll go over what the Ownership model means exactly, some of its nuances and see how Rust handles memory compared to other languages.

The Ownership Model

Let's get to the crux of the issue. The 3 rules that govern Ownership in Rust:

  • Each value in Rust has an owner.

  • There can only be one owner at a time.

  • When the owner goes out of scope, the value will be dropped.

If you keep these in mind, you’re already halfway to understanding how Ownership works in Rust.

Deep Dive

Let’s look at how other programming languages handle memory:

  • Garbage Collector: In Java or C#, the programmer does not have control over memory and it's handled by a dedicated Garbage collector. Makes your life easier, but takes away fine-grain control.

  • Manual Memory Management: In C or C++, programmers have to manually allocate/deallocate memory. This can cause a lot of errors and bugs, especially from a security point of view.

  • And then there’s the Ownership Model

Now that you understand these forms, it’ll be easier to grasp how the memory in Rust works.

2 essential concepts to keep in mind.

  • Stack — Memory location available to our program that works on a LIFO(Last In First Out) basis. This means the data that entered last, will be removed first. Think of it like a stack of books. All data stored here have fixed size during compile-time.

  • Heap — Variables whose size is not known at compile time are stored here among others. In most cases, variables stored in the stack have a pointer attached to the actual value in the heap.

Scope

Scope is a term often used in programming to denote a specific portion between which a variable lives. Outside this portion, the variable is non-existent.

fn main() {
   {      //scope starts
       let a:i32=5;
       println!("{}",a);      //prints 5  
   }      //scope ends
// 'a' does not exist here

}

So the value of the variable a is dropped automatically once the scope in which it was declared in is over.

Copy & Move

Data stored in the stack can be copied to another variable. However, data stored in the heap cannot be copied; it can be “moved”. This means the ownership of that piece of data is changed.

This code will throw an error. Here’s why

fn main() {
    let a:i32=5;
    let b:i32=a;
    let s1 = String::from("Hello World");
    let s2 = s1; // Ownership transferred or 'moved'

    println("{}",a);        //prints 5
    println("{}",b);        //prints 5
    // Error: 's1' no longer accessible
    println!("{}", s1);
    //will print "Hello World"
    println!("{}", s2);
}

a and bare stored in the stack since their sizes are known i.e. 32 bits. So, the value of a can be copied into b

However, s1 is stored in the heap and this means it cannot be copied. When we assign the value of s1 to s2 ,we are moving the pointer from s1 to s2. Thus, s1 no longer points to “Hello World”

But why can’t you copy data from the heap, you may ask?

If we think back to the differences between the stack and heap, we remember that the size of data stored on the heap is not known at compile time, which means we need to run through some memory allocation steps during runtime. This can be expensive. Depending on how much data is stored, we could quickly run out of memory if we keep making copies of data without caution.

Does this mean we can NEVER copy heap data?

The answer is No — You can copy heap data, but it comes with consequences. There’s a handy method clone() that helps you do exactly that. But this will create a deep copy, which means it will allocate the same data again to the heap. As stated, memory allocation/deallocation in the heap is slower than the stack and thus more expensive. Hence, clone() comes with performance issues.

Hence, the following code will run successfully

fn main() {
    let s1 = String::from("Hello World");
    let s2 = s1.clone();    //Deep clone

    // Will print "Hello World"
    println!("{}", s1);
    //  Will print "Hello World"
    println!("{}", s2);
}

Borrowing and Referencing

Let's look at another concept — Borrowing. It allows us to use another variable’s data without having to copy it. Borrowing creates a pointer to the variable whose data we want.

fn main() {
    let s1 = String::from("Hello Rust");
    let s2 = &s1;        //Borrowing value of s1(immutable)

    // Will print "Hello Rust"
    println!("{}", s1);
    //  Will print "Hello Rust"
    println!("{}", s2);
}

Rust allows multiple immutable references to be alive at the same time. This is called read-only referencing. However, only 1 mutable reference can be alive at a particular point in time. This ensures memory safety and prevents read-write errors or data races.

But keep in mind, if the original variable goes out of scope or its value is moved, the borrowed value will be dropped as well.

fn main() {
    let  s1 = String::from("Hello Rust");
    {
        let s2 = &s1;
        println!("{}", s2);     //Will print "Hello Rust"
        let s3 = s1;
        println!("{}", s2);     //Will throw error value of s1 is moved
    }
}

Initially, s2 points to s1 and its value is owned by s1 itself. When s3 is initialized, the value is moved from s1 to s3 . Now, s2 is still pointing to s1 ,but there is no value associated with s1 .Hence, it will throw an error if you try to use either s1 or s2

That’s it for this post. I hope you have a better understanding of what the Ownership Model is and how Memory is managed in Rust.

For more informative posts, follow me on Twitter

Thank you for reading 🎉