Ownership in Rust for Typescript devs
Rhl
Posted on September 28, 2024
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:
- Each value in Rust has a single owner.
- There can only be one owner of a value at a time.
- 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
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 callsdrop
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
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
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
In this example:
-
takes_ownership
takes ownership of the strings1
. - 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`
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
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"
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:
- You can have either one mutable reference or any number of immutable references, but not both.
- 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
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.
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
November 29, 2024