Simplifying the Concept of Ownership and Borrowing in the Rust Programming Language
The Rustlang book introduces Ownership with the following:
Ownership is a set of rules that govern how a Rust program manages memory. All programs have to manage the way they use a computer’s memory while running. Some languages have garbage collection that regularly looks for no longer-used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks.
How do computers handle memory?
In computing, data is managed in the memory in two ways: Stacks or Heaps. Both the stack and heap are memories available for your computer to use at runtime.
Stack adds data to memory in a sequential way, referred to as a first-in and last-out model. When you create a memory that has a definite size the computer creates a stack to manage it. Stacks work like a pile of tiles. The first tile on the stack can be accessed last, while the last tile to enter the stack can be removed first. To add data to a memory location in a stack you “Push” and to remove from the stack you “Pop”.
A Heap on the other hand is less organized and is created when no memory size is defined for a set of values. So the computer finds a big enough location that can contain the data and creates a pointer to the address. The process of creating Heaps is called “Allocation”. Stacks are created by pushing, while a Heap is created by allocating memory. Pushing to the stack is faster than allocating on the heap because the allocator never has to search for a place to store new data;
Because the size of the pointer to the heap is known, it can be stored on a stack. But when you need the actual data you have to follow the pointer to its location in memory. When you call a function in your code, the different parameters, local variables, and pointers to heaps, etc get stored on a stack and get popped after being used. Ownership in Rust saves the developer from all the problems that could arise from not handling memory safely, from figuring out heap duplicates to cleaning memory, etc.
Ownership Rules in Rust
- Each value has an owner in Rust.
- There can only be one owner at a time.
- When an owner goes out of scope the value is dropped.
Example:
let s = “Hello World”;
The above example is a string literal, where the value of the string is bound to s. In this case, the computer knows the exact size at compile time and this would go to a stack rather than a heap. For more complex types like strings which are capable of mutating, they will go on a heap.
let mut s = String::from(“hello”);
This has its own set of challenges because this String type can be mutated, hence it would end in a heap where its content is being pointed to somewhere in the memory.
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`
The above example shows that ‘s’ can expand from “hello” to “hello world”. This kind of dynamism requires that to have codes that are relatively safer, memory must be treated efficiently.
Every string in Rust has 3 major properties; A pointer to the location of its value in memory, the length which is the current size in bytes, and the capacity which is the total size in bytes assigned to the string by the allocator. So unlike in integers where we can assign copies of a variable to another variable, that would be expensive with strings because like we said they are not strictly bound to stacks but point to different heaps.
To ensure that strings can conveniently interact with each other, without memory issues, the previously stated rules ensure that Rust is able to handle memories without the need for garbage collection. In the example below, the code looks familiar, and while it can work in other languages, Rust would flag it as an error.
let s1 = String::from(“hello”);
let s2 = s1;
println!("{}, world!", s1);
This is because when we bind s1 to s2, what we essentially did was transfer ownership. Rust does not do a copy like in other languages, because it tries to suppress the possibility of a double-free memory, where two variables try to free the same memory at the same time. Instead of copying Rust moves the properties of s1 to s2 and s1 becomes invalidated automatically, hence calling it outputs and error.
But, if you want to deeply copy s1 to s2 without shifting ownership, Rust has a method for doing that called clone.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
While the above case works well for string data type, there is a twist. The same does not apply to integers. Remember that at compile time, the size of an integer is known, the same as a string literal. As a result binding one integer variable to another variable works in a stack and copying happens automatically and easily, hence no need to move the value or clone.
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
References and Borrowing
A reference is like a pointer to an address, in that we can follow it to the variable that actually owns the value. The idea of referencing is that instead of moving ownership from one variable to the next, we can reference the value in one variable to another variable and use that value to carry out some computation without changing anything. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.
fn main() {
let s1 = String::from(“hello”);
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
The function calculate_length accepts the reference to string, so instead of taking ownership of s1, it references it for use, while s1 still remains valid. The act of referencing a variable without taking ownership of it is called borrowing.
You might be wondering if the value in a reference can be changed since it is just pointing to an address and its type in memory… Well yes!
To do that Rust uses the &mut keyword. The value you are referencing must first be declared as mutable, and then your reference has to expressly state that it is referencing that variable as being mutable. This way, the above function could be rewritten as:
fn main() {
let mut s1 = String::from("hello");
let len = calculate_length(&mut s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &mut String) -> usize {
s.len()
}
This expressly states that we are referencing the value as mutable, thus allowing the function which is borrowing it to mutate it.
let mut s = String::from(“hello”);
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!(“{}, {}, and {}”, r1, r2, r3);
One restriction is that you cannot reference a value as mutable more than once. This prevents a situation where two events simultaneously try to write to a particular address, thus preventing data races. You are not also allowed to declare a mutable reference after defining other immutable references, because it would cause issues if the value they are referencing suddenly changes under them. To write the above, r1 and r2 must first be used before stating r3.
let mut s = String::from(“hello”);
let r1 = &s; // no problem
let r2 = &s; // no problem
println!(“{} and {}”, r1, r2);
// variables r1 and r2 will not be used after this pointlet
r3 = &mut s; // no problem
println!(“{}”, r3);
The above code compiles successfully because the scope of r1 and r2 ends after being used at println!. So r3 can be conveniently declared after them.
To learn more about the concept of ownership, referencing, and borrowing; here is the resource which I used as a guide for writing this article which you will surely find as being very valuable.
https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
Bon, appetite!!!