Ownership in Rust for Typescript devs

rhl314

Rhl

Posted on September 28, 2024

Ownership in Rust for Typescript devs

Hi, I am Rahul and I learnt rust to build Loadjitsu.io. This is the fifth post in my series on "Rust for typescript devs".
Read the third part about Functions here.
Read the intro post here.

Basics of ownership in Rust

Ownership is one of the most fundamental and unique concepts in Rust, setting it apart from many other programming languages, including TypeScript. Rust’s ownership model helps ensure memory safety without needing a garbage collector, making it highly efficient while preventing common errors like data races, null pointer dereferencing, or dangling pointers. In this section, we’ll dive deep into the concept of ownership in Rust, explore its key rules, and explain how it works with practical examples.

The Basics of Ownership

In Rust, ownership is the system that manages memory. The main idea is that each value in Rust has a variable that’s its owner, and there can only be one owner at a time. When the owner goes out of scope, Rust automatically deallocates the memory associated with that value.

Here are the three key rules of ownership in Rust:

  1. Each value in Rust has a single owner.
  2. There can only be one owner of a value at a time.
  3. When the owner goes out of scope, the value is automatically dropped.

This ownership system ensures that memory is cleaned up efficiently, without needing a garbage collector or manual memory management, as is required in some other languages like C or C++.

Example of Ownership

Let’s start with a simple example to show ownership in action.


{
    let s = String::from("hello"); // `s` owns the string "hello"
    // We can use `s` here
} // When `s` goes out of scope, "hello" is automatically dropped

Enter fullscreen mode Exit fullscreen mode

In this example:

  • The String::from("hello") allocates memory on the heap for the string "hello".
  • The variable s becomes the owner of that memory.
  • When s goes out of scope (i.e., at the end of the block), Rust automatically calls drop to free the memory.

Ownership ensures that memory is automatically cleaned up when it's no longer needed, which is crucial for writing efficient programs without memory leaks.

Ownership and Moves

In Rust, when you assign a value from one variable to another, the ownership is moved from the original variable to the new one. After the move, the original variable is no longer valid.

Example:


let s1 = String::from("hello");
let s2 = s1; // Ownership of the string moves from `s1` to `s2`
// `s1` is no longer valid here

Enter fullscreen mode Exit fullscreen mode

After s1 is moved to s2, Rust considers s1 to be invalid, and trying to use s1 would result in a compile-time error. This ensures there’s no duplication of ownership, which would lead to memory safety issues.

Why does this happen?

Strings in Rust are stored on the heap, and when you move ownership from one variable to another, the new variable takes control of the heap-allocated data. By invalidating the original variable, Rust avoids potential issues like double frees (where two variables try to free the same memory) or dangling references.

What if You Need to Use the Original Variable?

If you want to continue using the original variable after assigning it to a new one, you can clone the data instead of moving it. Cloning creates a deep copy of the data on the heap, allowing both variables to own independent copies of the data.

Example of Cloning:


let s1 = String::from("hello");
let s2 = s1.clone(); // `s2` is a deep copy of `s1`
// Both `s1` and `s2` are valid here

Enter fullscreen mode Exit fullscreen mode

By using clone, both s1 and s2 are valid and independent of each other. While cloning allows you to retain both variables, it comes with a performance cost, as it involves copying the data.

Ownership and Functions

When passing values to functions, ownership behaves the same way: the function takes ownership of the value, and once the function completes, the value is dropped unless it is returned.

Example of Passing Ownership to a Function:


fn takes_ownership(s: String) {
    println!("{}", s);
} // `s` is dropped here

let s1 = String::from("hello");
takes_ownership(s1); // `s1` is moved to the function and is no longer valid here

Enter fullscreen mode Exit fullscreen mode

In this example:

  • takes_ownership takes ownership of the string s1.
  • After the function call, s1 is no longer valid, because its ownership was moved into the function, and it was dropped when the function finished.

Returning Ownership from Functions

If you want to pass ownership back after a function call, you need to return the value. This allows you to transfer ownership between different scopes.

Example of Returning Ownership:


fn takes_and_gives_back(s: String) -> String {
    s // Return ownership back to the caller
}

let s1 = String::from("hello");
let s2 = takes_and_gives_back(s1); // Ownership is moved to `s2`

Enter fullscreen mode Exit fullscreen mode

Here, ownership of s1 is passed to the function and returned back to s2. After this, s1 is invalid, but s2 can be used.

Borrowing and References

Often, you want to let a function use a value without taking ownership. Rust allows this via borrowing. Borrowing allows you to pass references to data without transferring ownership.

Example of Borrowing:


fn borrow_string(s: &String) {
    println!("{}", s);
}

let s1 = String::from("hello");
borrow_string(&s1); // Borrow `s1` without moving ownership
// `s1` is still valid here

Enter fullscreen mode Exit fullscreen mode

In this example, the function borrow_string takes a reference to the string (&String), allowing the function to use the string without taking ownership of it. This means s1 remains valid after the function call.

Mutable Borrowing

If you need to modify the borrowed value, you can use a mutable reference.

Example of Mutable Borrowing:


fn modify_string(s: &mut String) {
    s.push_str(", world");
}

let mut s1 = String::from("hello");
modify_string(&mut s1); // Borrow `s1` mutably
println!("{}", s1); // Output: "hello, world"

Enter fullscreen mode Exit fullscreen mode

Here, modify_string takes a mutable reference (&mut String), allowing it to modify the original string. With mutable references, Rust ensures that you can only have one mutable reference to a value at a time, which prevents data races in concurrent programs.

Rules of Borrowing

There are a few rules to follow when working with references and borrowing in Rust:

  1. You can have either one mutable reference or any number of immutable references, but not both.
  2. References must always be valid. They cannot outlive the value they refer to.

These rules ensure that Rust’s memory safety guarantees are upheld.

Dangling References

Rust prevents dangling references, which occur when a reference points to invalid memory. The borrow checker in Rust ensures that references are always valid by enforcing strict lifetime rules.

Example of a Potential Dangling Reference:


let r;
{
    let s = String::from("hello");
    r = &s; // Error: `s` does not live long enough
}
// `r` would be a dangling reference here

Enter fullscreen mode Exit fullscreen mode

In this case, s goes out of scope at the end of the inner block, making r a dangling reference. Rust’s borrow checker catches this at compile time, preventing the error.

💖 💪 🙅 🚩
rhl314
Rhl

Posted on September 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related