1 Month of Learning Rust - Ownership

1 Month of Learning Rust - Ownership

Daily short news for you
  • Privacy Guides is a non-profit project aimed at providing users with insights into privacy rights, while also recommending best practices or tools to help reclaim privacy in the world of the Internet.

    There are many great articles here, and I will take the example of three concepts that are often confused or misrepresented: Privacy, Security, and Anonymity. While many people who oppose privacy argue that a person does not need privacy if they have 'nothing to hide.' 'This is a dangerous misconception, as it creates the impression that those who demand privacy must be deviant, criminal, or wrongdoers.' - Why Privacy Matters.

    » Read more
  • There is a wonderful place to learn, or if you're stuck in the thought that there's nothing left to learn, then the comments over at Hacker News are just for you.

    Y Combinator - the company behind Hacker News focuses on venture capital investments for startups in Silicon Valley, so it’s no surprise that there are many brilliant minds commenting here. But their casual discussions provide us with keywords that can open up many new insights.

    Don't believe it? Just scroll a bit, click on a post that matches your interests, check out the comments, and don’t forget to grab a cup of coffee next to you ☕️

    » Read more
  • Just got played by my buddy Turso. The server suddenly crashed, and checking the logs revealed a lot of errors:

    Operation was blocked LibsqlError: PROXY_ERROR: error executing a request on the primary

    Suspicious, I went to the Turso admin panel and saw the statistics showing that I had executed over 500 million write commands!? At that moment, I was like, "What the heck? Am I being DDoSed? But there's no way I could have written 500 million."

    Turso offers users free monthly limits of 1 billion read requests and 25 million write requests, yet I had written over 500 million. Does that seem unreasonable to everyone? 😆. But the server was down, and should I really spend money to get it back online? Roughly calculating, 500M would cost about $500.

    After that, I went to the Discord channel seeking help, and very quickly someone came in to assist me, and just a few minutes later they informed me that the error was on their side and had restored the service for me. Truly, in the midst of misfortune, there’s good fortune; what I love most about this service is the quick support like this 🙏

    » 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

5 profound lessons

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? 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...