Ownership

Tushar PamnaniTushar Pamnani
6 min read

Welcome to the world of Rust! One of the most important concepts in Rust is ownership. It might sound intimidating, but it's really just a way for Rust to manage memory safely and efficiently, without needing a garbage collector like some other languages.

Think of it like this: Imagine you have a pet dog.

  • The dog is like a piece of data in your program (e.g., a string, a number, a struct).

  • The owner is the variable that's responsible for that data.

Only one person can own the dog at a time. When that person no longer wants the dog (goes out of scope - we'll get to that), they have to take care of it, meaning they can’t abandon the dog! This way we prevent double frees/ frees of bad memory.

Why is this important? Because without a system like this, you can run into problems like:

  • Memory leaks: Forgetting to "take care" of your data when you're done with it.

  • Dangling pointers: Trying to use data after it's already been freed.

Rust's ownership rules prevent these issues, making your code safer and more reliable.

The Big Picture: Managing Memory Without a Garbage Collector

Many programming languages (like Java or JavaScript) use a "garbage collector" to automatically clean up memory. This is convenient, but it can sometimes slow things down. Rust takes a different approach. It uses ownership to manage memory at compile time. This means Rust checks your code before you run it to make sure there are no memory errors. This leads to faster and more predictable performance, because there's no garbage collector running in the background. Refer to Memory Management (coming soon) for more context!

Key Concepts: The Rules of the Game

Ownership comes down to a few simple rules:

  1. Each value has an owner: Every piece of data in Rust has a variable that "owns" it.

  2. One owner at a time: Only one variable can own a particular piece of data at any given time.

  3. Owner goes out of scope, value is dropped: When the owner variable goes out of scope (e.g., the function it's in finishes executing), the data is automatically dropped (freed from memory).

Let's explore what "scope" means. Scope is simply the region in the program for which the item is valid.

fn main() {
    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
        println!("{}", s);
    }                      // this scope is now over, and s is no longer valid
}

In this case, s is only available in between { and }.

A Concrete Example: Strings in Rust

Let's look at a common example: strings. In Rust, there are two main types of strings:

  • &str: A string slice (a reference to a string that's stored somewhere else). These are immutable and their size is known at compile time.

  • String: A growable, mutable, owned string. This is what we'll focus on for ownership.

fn main() {
    let my_string = String::from("hello"); // my_string owns the string data
    println!("{}", my_string);
}

In this example, my_string owns the string "hello". When my_string goes out of scope (at the end of the main function), the memory used by the string "hello" will be automatically freed.

Ownership in Action: Moves

Let's see what happens when you assign one String to another:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}", s2); // This works fine
    // println!("{}", s1); // This will cause a compile error!
}

What's going on here? When we assign s1 to s2, ownership of the string data is transferred from s1 to s2. This is called a move. s1 is no longer valid after the move because it no longer owns the data. Rust does this to prevent a "double free" error, which would happen if both s1 and s2 tried to free the same memory when they went out of scope.

If you uncomment the line println!("{}", s1);, the compiler will complain because you're trying to use s1 after it's been moved.

Cloning: Making a Copy

Sometimes, you do want to make a copy of the data, rather than transferring ownership. In that case, you can use the .clone() method:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("{}", s1); // This now works!
    println!("{}", s2); // And this works too!
}

Now, both s1 and s2 own separate copies of the string "hello". This is useful when you need to keep the original data around. However, cloning can be slower than moving, especially for large amounts of data, as it requires allocating new memory and copying the data.

Ownership and Functions

Ownership also affects how you pass values to functions:

fn main() {
    let my_string = String::from("hello");
    takes_ownership(my_string);
    // println!("{}", my_string); // This will cause a compile error!
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

When you pass my_string to takes_ownership, ownership of the string data is moved to the some_string parameter. After the function call, my_string is no longer valid in main. Again, if you uncomment the println! line, you'll get a compile error.

If you wanted to use my_string after the function, there are a few options. You can either .clone() it before passing it to the function, or have the function return ownership of the string:

fn main() {
    let my_string = String::from("hello");
    let my_string = takes_ownership(my_string); //Get ownership back

    println!("{}", my_string); // This now works!
}

fn takes_ownership(some_string: String) -> String {
    println!("{}", some_string);
    some_string // Return ownership
}

How Ownership Works Under the Hood

Let's briefly explore what's happening behind the scenes (simplified!).

When you create a String, Rust allocates memory on the heap to store the string data. The String variable itself (the owner) stores a pointer to this memory on the heap, the capacity, and the length.

When a move happens, Rust doesn't copy the data on the heap. Instead, it copies the pointer, length, and capacity from one String variable to another and marks the original variable as invalid. This is much faster than copying the entire string data.

When a String goes out of scope, Rust automatically calls a "drop" function, which frees the memory on the heap that the String was pointing to.

Here's a simplified sequence diagram of a move:

sequenceDiagram
    participant S1 as s1
    participant S2 as s2
    participant H as Heap

    S1->>H: Create "hello"
    H-->>S1: Return pointer

    Note right of S1: s1 owns "hello"

    S1->>S2: Move ownership

    Note right of S2: s2 now owns "hello"
    Note right of S1: s1 is invalid

Here's how a simple struct definition might look like, it contains a pointer and length:

struct StringData {
    ptr: *mut u8, // Pointer to the data on the heap
    len: usize,   // Length of the string in bytes
    capacity: usize, // Allocated capacity
}

When you assign s2 = s1, Rust copies the StringData (the pointer, length, and capacity), and then makes s1 invalid!

Beyond the Basics: Borrowing

You might be thinking, "This ownership stuff sounds restrictive! What if I just want to use some data without taking ownership?" That's where borrowing comes in! We'll cover borrowing and references in the next chapter.

Conclusion

Ownership is a core concept in Rust that ensures memory safety without a garbage collector. By understanding the rules of ownership, moves, and cloning, you can write safer and more efficient Rust code. In the next chapter, we'll learn about borrowing and references, which provide a way to access data without taking ownership.

0
Subscribe to my newsletter

Read articles from Tushar Pamnani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tushar Pamnani
Tushar Pamnani

I am a MERN stack developer and a blockchain developer from Nagpur. I serve as Management Lead at ML Nagpur Community and The CodeBreakers Club, RCOEM.