1 Month of Learning Rust - Ownership

1 Month of Learning Rust - Ownership

Daily short news for you
  • For a long time, I have been thinking about how to increase brand presence, as well as users for the blog. After much contemplation, it seems the only way is to share on social media or hope they seek it out, until...

    Wearing this shirt means no more worries about traffic jams, the more crowded it gets, the more fun it is because hundreds of eyes are watching 🤓

    (It really works, you know 🤭)

    » Read more
  • A cycle of developing many projects is quite interesting. Summarized in 3 steps: See something complex -> Simplify it -> Add features until it becomes complex again... -> Back to a new loop.

    Why is that? Let me give you 2 examples to illustrate.

    Markdown was created with the aim of producing a plain text format that is "easy to write, easy to read, and easy to convert into something like HTML." At that time, no one had the patience to sit and write while also adding formatting for how the text displayed on the web. Yet now, people are "stuffing" or creating variations based on markdown to add so many new formats that… they can’t even remember all the syntax.

    React is also an example. Since the time of PHP, there has been a desire to create something that clearly separates the user interface from the core logic processing of applications into two distinct parts for better readability and writing. The result is that UI/UX libraries have developed very robustly, providing excellent user interaction, while the application logic resides on a separate server. The duo of Front-end and Back-end emerged from this, with the indispensable REST API waiter. Yet now, React doesn’t look much different from PHP, leading to Vue, Svelte... all converging back to a single point.

    However, the loop is not bad; on the contrary, this loop is more about evolution than "regression." Sometimes, it creates something good from something old, and people rely on that goodness to continue the loop. In other words, it’s about distilling the essence little by little 😁

    » Read more
  • Alongside the official projects, I occasionally see "side" projects aimed at optimizing or improving the language in some aspects. For example, nature-lang/nature is a project focused on enhancing Go, introducing some changes to make using Go more user-friendly.

    Looking back, it resembles JavaScript quite a bit 😆

    » Read more

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!

Premium
Hello

The secret stack of Blog

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

View all

Subscribe to receive new article notifications

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

Comments (0)

Leave a comment...