Learning Rust Through Documentation — Part V
Finally Understood Rust Ownership!
Ownership, One of the most difficult topics, After talking to Gemini for a few hours, I feel like I finally got a hang of Rust Ownership, I’ll try to keep it as simple as possible.
So, when we run a program, there are two areas in the memory where we can store values, The Stack & The Heap. The stack is where we keep the data which we’re sure about, how much space it would take, etc. And the Heap is where we keep the data whose size we’re unsure of. The only types that stay on the heap are String & Vec. String and &str both are used to store a string but are two very different data types, which we’ll be looking into later.
So, What is Ownership?
That’s Rust’s way of managing memory. It is a set of compiler enforced rules that ensure memory safety and prevent common bugs like dangling pointers, data races, etc. There are three rules to it:
- Each value in Rust has an owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Rust’s Ownership Rules primarily apply to the values stores on the heap as they involve dynamic memory allocation, and deallocation, which need to be managed carefully to avoid bugs.
Stack allocated data like intergers, floats, booleans and references are uaually stored directly on the stack. The stack is managed automatically so ownership is applied for them too but isn’t as critical for these types.
Ownership Rules and the heap:
- Each value on the heap has a single owner at any given moment.
- When the owner goes out of scope, The value is dropped (memory deallocated).
- You can move ownership from one variable to another.
Ownership Rules and the Stack:
- Values on the stack don’t have explicit ownership in the same way as heap-allocated data.
- Their lifetimes are tied to the scope in which they are declared.
- They are often copied when passed to functions, and their memory is automatically reclaimed when they go out of scope.
The Data Types Can Be Categorised in Two Types:
- Non-copy Types: When we return a value that does not implement the Copy trait (like String, Vec, or custom structs), ownership is transferred to the caller. And the original variable in the function becomes invalid.
- Copy Types: For types that implement the Copy trait (like &str, integers, booleans, or floats), the value is copied, and the original variable remains untouched and hence also valid.
Let’s get working with the code to get a better understanding of what’s happening under the hood. We’ll be skipping the boilerplate code unless necessary.
let mut s = "Hello";
s.push_str(", World!"); // tried appending a literal to &str but throws error
// println!("{s}"); // Compilation won't get till here
The above code will throw an error on the second line and the code won’t compile. This is because the data “Hello” is stored on the stack and the data on the stack as we discussed is immutable as there’s a limitation of data on the stack. So, the type of the variable here is &str.
let mut s = String::from("Hello");
s.push_str(", World!"); // appends a literal to a String
println!("{s}"); // prints `Hello, World!`
The code above will run fine. This is because when we initialize a string using the String::from() method, It makes a new object of the type String which is allocated on the heap. And as the heap has free space which we can work upon, we can modify the string.
Stack and Heap is the difference between the types &str and String. And Is crucial for understanding memory management in Rust. Think of it as a stack of plates vs an open playground. I Know, not a great analogy, but you’ll get it.
So, when we have a stack of plates, suppose the variable b has 8 plates, the variable a has 3 plates, When the 3 plates of a are removed, that’s when we can remove the 8 plates of b. And we can’t insert a plate in between and then suddenly say that b has 9 plates. This makes it tough to keep a record of the things and their respective places and might also damage other plates in the process. So, we don’t allow adding additional plates (memory locations) to a variable when it comes to stack. This what a stack memory is like.
But when it comes to a ground, you can assign an area to a number of people, if additional people of your group come, you can request a specific amount of area as per your needs. This is usually less efficient as there’s the need to find area according to the needs of the variable. So this basically is what a heap memory is like.
Always keep in mind that complex data structures like String, Vector, etc. live on the heap.
What is Borrowing?
As the name suggests, It means borrowing the value of the variable for some purposes and returning it back. So what the returning it back means is that the actual owner won’t lose ownership when the String is borrowed. Example:
fn main(){
let a = String::from("Hello");
let string_len = calculate_len(&a);
println!("The Length of the String is {string_len}");
}
fn calculate_len(x: &String) -> usize {
let length = x.len();
length
}
As this is borrowing, The String Object here is immmutable by default, so if you try to make modifications to the string like using
a.push_str(", World!")
It won’t work, as we’ve used the default borrow, But there’s another thing we can do, A mutable borrow. For the same, We need to just mention the word mut while passing the variable. Example:
fn main(){
let mut a = String::from("Hello");
mod_string(&mut a);
println!("{a}");
}
fn mod_string(x: &mut String){
x.push_str(", People!")
}
A Restriction to avoid race conditions
In Rust, We can’t have another references to a value if we already have a mutable reference of the same. Let’s go with an analogy. If you and your friend are going to a concert, and you both have references to the same ticket and there are multiple entries. At your entry you claimed that you have your ticket, but your friend has a mutable reference, which means that he can change the details. So the only time you’ll be allowed entry is that you enter first and then the friend changes the details and enters. But you can’t communicate with your friend as it’s not entirely in your hands. It’s in the hand of the scheduler. So it would cause a problem when you’re entering but your friend has already entered with the details changed as it won’t match your ID.
So, this is why rust doesn’t allow you to make multiple references given that one of the reference is mutable (data can be changed).
Example:
fn main(){
let mut a = String::from("Ticket!");
let s1 = &a;
let s2 = &a;
println!("a: {a}, s1: {s1}, s2: {s2}")
}
The below code won’t work:
fn main(){
let mut a = String::from("Ticket!");
let s1 = &a;
let s2 = &mut a;
println!("a: {a}, s1: {s1}, s2: {s2}");
}
Here’s What Works and What Doesn’t:
- n Number of Immutables : Works
- n Number of Immutables With ≥ 1 Number of Mutables : Doesn’t
- n Number of Mutables Where n ≥ 2 : Doesn’t
Remember I mentioned dangling pointers and race condition, The thing we discussed above is a race condition, Let’s get to dangling pointers and see how Rust handles it.
Dangling pointer is the concept of the pointing to a value which doesn’t exist anymore. Example:
fn main(){
let s = dangling_pointer();
}
fn dangling_pointer() -> &String{
let a = String::from("Hello People!");
&a
}
Here, We’re returning the pointer to the String object a. The problem is that a will become invalid as soon as the function dangling_pointer() terminates. So, the Object will be deallocated. And the variable s will be pointing to an empty Object.
Let’s try running this and see what happens.
As expected, the compiler handles throws an error suggesting us things to correct the code. Thus not letting us write unsafe code.
This is why Rust is known for its safety.
That’s All For Today, See You In The Next Blog!
Make sure to let me know If you liked the blog, You can reach me on LinkedIn and here on Medium. Make sure to follow and like If the blog helped.