1 Month of Learning Rust - Data Mutability and Borrowing Ownership

1 Month of Learning Rust - Data Mutability and Borrowing Ownership

Daily short news for you
  • Since the Lunar New Year holiday has started, I won't be posting anymore. See you all after the holiday! 😁

    » Read more
  • Continuing about jj. I'm wondering if there are any GUI software made for it yet to make it easier to use. There are already so many similar to git that I can't count them all.

    Luckily, the author has compiled them all together in Community-built tools around Jujutsu 🥳

    » Read more
  • Turso announces that they are rewriting SQLite in Rust. This adds another piece of evidence supporting the notion that Rust is "redefining" many things.

    But the deeper reason is more interesting. Why are they doing this? Everyone knows that SQLite is open source, and anyone can create a fork to modify it as they wish. Does the Turso team dislike or distrust C—the language used to build SQLite?

    Let me share a bit of a story. Turso is a provider of database server services based on SQLite; they have made some customizations to a fork of SQLite to serve their purposes, calling it libSQL. They are "generous" in allowing the community to contribute freely.

    Returning to the point that SQLite is open source but not open contribution. There is only a small group of people behind the maintenance of this source code, and they do not accept pull requests from others. This means that any changes or features are created solely by this group. It seems that SQLite is very popular, but the community cannot do what they want, which is to contribute to its development.

    We know that most open source applications usually come with a "tests" directory that contains very strict tests. This makes collaboration in development much easier. If you want to modify or add a new feature, you first need to ensure that the changes pass all the tests. Many reports suggest that SQLite does not publicly share this testing suite. This inadvertently makes it difficult for those who want to modify the source code, as they are uncertain whether their new implementation is compatible with the existing features.

    tursodatabase/limbo is the project rewriting SQLite in Rust mentioned at the beginning of this article. They claim that it is fully compatible with SQLite and completely open source. Limbo is currently in the final stages of development. Let’s wait and see what the results will be in the future. For a detailed article, visit Introducing Limbo: A complete rewrite of SQLite in Rust.

    » Read more

The Issue

In the previous article, we learned about the concept of ownership in Rust. Thanks to ownership, Rust knows when a variable is no longer in use and can free up memory. By using & before the variable name, we declare to Rust that we're just "borrowing" the value temporarily without transferring ownership, so the borrowed variable still exists after borrowing, but with some limitations.

Today's article will delve into how Rust deals with data borrowing behavior and how it prevents "non-standard" behaviors with borrowed and mutable data during compile time, not runtime.

Rust Avoids Borrowing and Mutating Data Simultaneously

First, let's take a look at how Rust handles memory in this example program.

let mut v: Vec<i32> = vec![1, 2, 3];
v.push(4);

Rust Avoids Borrowing and Mutating Data Simultaneously

At line L2, adding an element to a list causes a mutation, but instead of adding 4 to the next memory location, Rust copies the entire data to another memory location, causing the data in the heap of v at L1 to be freed, and v now points to a different memory location.

What would happen with the following program?

let mut v: Vec<i32> = vec![1, 2, 3];
let num: &i32 = &v[2];
v.push(4);
println!("Third element is {}", *num);

Here, num is borrowing the value at position 2 of v through &v[2]. But after the push operation, the original heap data of v has been deallocated. So, where does num point to? You guessed it right; this program would result in an error.

Rust Avoids Borrowing and Mutating Data Simultaneously - 2

Therefore, Rust enforces the safety principle of pointers: data can never be both borrowed and mutated at the same time.

To introduce borrowing, Rust has established rules to ensure the "Pointer Safety Principle," which is enforced by the borrow checker tool.

Borrow Checker

The core idea behind Rust is the concept of three ownership rights for a variable's data.

  • Read (R): Data can be copied to another location.
  • Write (W): Data can be modified.
  • Own (O): Data can be moved or deleted.

These rights only exist at compile time, meaning the compiler "independently derives" these rights to check whether your program is valid before building it into binary.

By default, a variable has R and O rights over its data. If a variable is declared as mut, it gains W rights.

Let's consider how Rust checks data rights in the following program.

Rust Checks Rights

First, v is declared as mut, so it has R, O, and W rights. Then, num borrows the value of v, and now v loses 2 rights: W and O, while num gains R and O rights over the borrowed data. *num is a variable reading the value at the location it borrowed, so it only has R rights.

Right after the println! statement, num is released, restoring all rights to v, but num and *num lose all rights to the borrowed data.

After the push statement, v is no longer in use, so it also loses all rights to its data.

Rust calls everything to the left of the assignment (=) as paths. Thus, ownership rights are determined on paths, not just on variables. Paths include:

  • Variables, like a.
  • *a.
  • Accessing elements in an array: a[0].
  • a.0 of tuples or a.field of structs.
  • And some more complex accesses like *((*a)[0].1)!?.

So, can a variable borrow data from another variable and modify that data? Remember the concept: "Data can never be both borrowed and mutated at the same time."

let mut v: Vec<i32> = vec![1, 2, 3];
let num: &mut i32 = &mut v[2];
*num += 1;
println!("Third element is {}", *num);
println!("Vector is now {:?}", v);

Instead of &v[2], we now declare &mut v[2] to indicate that num has ownership rights (O) over the borrowed data. The variable *num directly reads data from the heap and increments it by 1. In the end, v is also modified according to num.

Compared to the initial example, v loses all rights when num borrows data with O rights. This ensures data safety and adheres to the principle of "Data can never be both borrowed and mutated at the same time."

Data Must Outlive Its References

Let's start with an example:

let s = String::from("Hello world");
let s_ref = &s;
drop(s);
println!("{}", s_ref);

The code is attempting to drop s while s_ref is borrowing its value. Remember the concept mentioned earlier: "Data can never be both borrowed and mutated at the same time." So, drop cannot proceed because it requires O rights. This is why the program results in an error.

Both we and Rust can quickly identify the lifetime of s. However, programs are not always straightforward, and Rust needs a way to determine the lifetime of a variable in the program to prevent situations like the one above.

Consider this function:

fn first_or(strings: &Vec<String>, default: &String) -> &String {
    if strings.len() > 0 {
        &strings[0]
    } else {
        default
    }
}

The function can return either the first String from the strings parameter or the default parameter. What would happen if we call the function like this?

fn main() {
    let strings = vec![];
    let default = String::from("default");
    let s = first_or(&strings, &default);
    drop(default);
    println!("{}", s);
}

Since strings is empty, s will borrow from default. However, a drop(default) statement occurs before printing s. Therefore, this program is not safe and will result in an error.

Another example of an unsafe program:

fn return_a_string() -> &String {
    let s = String::from("Hello world");
    let s_ref = &s;
    s_ref
}

s_ref is attempting to borrow the value of s, but when the function ends, s will be deallocated, making returning s_ref meaningless. Hence, this program will result in an error.

Conclusion

This concludes the article on ownership rights and how Rust checks borrowing data rights. In my opinion, this is one of the most challenging concepts in Rust, but it's crucial for writing and debugging programs. Readers can continue reading the article Fixing Ownership Errors to learn how to resolve some common ownership-related errors.

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 (2)

Leave a comment...
Avatar
Ẩn danh1 year ago
"lúc này v bị mất 2 quyền W, O, bù lại num có quyền R, O trên dữ liệu mà nó mới mượn được". References không chiếm quyền sở hữu, nó chỉ mượn (borrowing), giống như biến, ref cũng có 2 kiểu mượn là immutable vs mutable nhưng có phụ thuộc vào owner, nếu owner khai bao với mutable thì ref với được mutable
Reply
Avatar
Ẩn danh1 year ago
"Tại L2, hành động thêm một phần tử vào một danh sách đã gây nên đột biến, nhưng thay vì thêm 4 vào ô nhớ tiếp theo thì Rust đã sao chép toàn bộ dữ liệu sang một vùng nhớ khác". chỗ này không phải lúc nào cũng cũng được cấp phát mới vùng nhớ, nó phục thuộc vào cái cap của Vec, khi len > cap thì Vec mới được cấp phát với cap mới là x2, vì vậy để tối ưu chương trình nếu biết len của Vec cần sử dụng thì thường người ta sẽ khởi tạo nó với cap được xác định trước để tránh bị cấp phái lại nhiều lần
Reply
Scroll or click to go to the next page