1 Month of Learning Rust - Ownership

1 Month of Learning Rust - Ownership

The Issue

In JavaScript and some other languages, you often hear phrases like "memory leak." This happens when an application consumes more memory than it needs. There are various reasons for this issue, one of which is improper memory deallocation.

The challenge in languages with automatic garbage collection is how to release memory correctly and in the right place and time. The fundamental idea behind them is, "just keep running, and I'll take care of cleaning up the mess." But how do you know if a variable is still needed or not? That's the job of a garbage collector algorithm. Although we often hear about exciting garbage collector algorithms, sooner or later, memory exhaustion remains a problem.

Automatic Garbage Collection

I believe many of you are familiar with the syntax malloc and free in the C programming language. malloc allocates memory for a variable, and free deallocates it. The reason for this is that C does not have built-in automatic garbage collection, so we have to declare and free memory manually. This allows programmers to control the amount of memory that a program is allowed to use. However, what happens if they forget to free a variable they malloced, or if they free it twice? Manual memory allocation leads to human errors.

In Rust, we don't have automatic garbage collection, and there's no mechanism for manual memory allocation/deallocation. So, how does Rust manage memory?

Ownership

Let's start with a simple example.

fn main() {
    let a: i32 = 1;
    let b: i32 = a;
    println!("a = {}", a);
    println!("b = {}", b);
}

Running this program, we see two lines printed: "a = 1" and "b = 1". There's nothing strange about it; b = a clearly means that b is also equal to 1.

Now, let's move on to the next example.

fn main() {
    let a = String::from("a");
    let b = a;
    println!("a = {}", a);
    println!("b = {}", b);
}

Of course, the result is "a = a" and "b = a," right? But in reality, this program results in an error.

Due to Rust's ownership system, the value a initially holds a String with the value "a." After declaring b = a, we have effectively transferred ownership of the String "a" from a to b. As a result, a no longer holds any value, and it gets deallocated.

Hold on a second; in the first example, didn't a get assigned to b as well? It's quite simple in that case. i32 is a value stored on the stack, and when you assign one stack variable to another in Rust, it directly takes the value to assign. It's like declaring b = 1.

In contrast, a String is a value stored on the heap. Rust explains that copying values from the heap is costly and unsafe. Therefore, in combination with ownership, the value on the heap is transferred to the assigned variable. You have effectively transferred ownership. Rust does not allow the same behavior as in JavaScript, where assigning one variable to another creates a reference. In Rust, ownership is moved when necessary.

In Rust, there are several ways to create data stored on the heap, such as Vec, String, HashMap, or Box. Therefore, you should be cautious when working with any data stored on the heap to avoid losing ownership.

So, ownership in Rust is a principle of managing the heap, including:

  • All data on the heap must be owned by exactly one variable.
  • Rust frees heap data when its owner goes out of scope.
  • Ownership can be transferred by assignment or by passing it as a function parameter.
  • Heap data can only be accessed through its current owner.

Back to the example, is there a way to assign a value without transferring ownership? Simply use the "clone" method to make a copy.

let a = Box::new([0; 1_000_000]);
let b = a.clone();

clone copies the data to a different memory location and assigns it to b. Therefore, the assignment no longer transfers ownership, and a is not deallocated temporarily.

Ownership transfer not only occurs in value reassignment but also in function parameters. For example:

fn main() {
    let first = String::from("Ferris");
    let full = add_suffix(first);
    println!("{full}, originally {first}"); // first is now used here
}

fn add_suffix(mut name: String) -> String {
    name.push_str(" Jr.");
    name
}

add_suffix is a function that takes name of type String as a parameter. At first glance, you might expect the result to be "Ferris Jr., originally Ferris." However, in reality, the program produces an error.

In fact, when you call add_suffix(first), ownership of first is transferred to add_suffix, and then it is deallocated. Therefore, the println! statement to print the value of first results in an error.

You might wonder why the parameter of add_suffix is written as mut name: String. Recall that all variables are immutable by default in Rust. Only when declared with mut can they be assigned a new value. So, within the function, name.push_str(" Jr.") is allowed because push_str mutates the data.

To solve this issue, we can do the following:

fn main() {
    let first = String::from("Ferris");
    let (first, full) = add_suffix(first);
    println!("{full}, originally {first}"); // first is now used here
}

fn add_suffix(name: String) -> (String, String) {
    let mut suffix_name = name.clone();
    suffix_name.push_str(" Jr.");
    (name, suffix_name)
}

Instead of returning only name, add_suffix now returns both name and suffix_name. The idea here is to return both the original name and the new name so that we can reassign them using let (first, full) = add_suffix(first). In the end, we still have first and full in existence. However, this approach is not the best. Instead, Rust introduces the concept of References and Borrowing.

We can reference variables with data on the heap and "borrow" them using &.

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    greet(&m1, &m2);
    let s = format!("{} {}", m1, m2);
}

fn greet(g1: &String, g2: &String) {
    println!("{} {}!", g1, g2);
}

Although m1 and m2 are used as parameters for greet, they are just "borrowed" values. The variables declared as &m1 and &m2 are borrow variables; they point to borrowed variables without actually owning the values.

Borrowing Data in Rust

There might be some confusion here, but don't worry. I've had to read and reread the ownership concept many times, and I still don't fully grasp it. Therefore, I recommend checking out the Understanding Ownership section in the Rust documentation for a more detailed explanation.

Finally, the good news is that Rust has a powerful code checker. Remember, before running a program, we need to compile it into machine code. During the compilation process, Rust checks everything, such as syntax and ownership-related errors. So, Rust will immediately remind you if you've done something wrong.

Summary

In this article, I've introduced the concept of ownership in Rust, which is a principle for managing heap memory since Rust lacks automatic garbage collection. However, there's more to it, and we'll delve deeper into the principles of ownership in the next article!

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.
Author

Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.

Did you find this article helpful?
NoYes

Comments (0)

Leave a comment...