Ahoy, fellow coders! On Day 9 of my #100DaysOfCode adventure with Rust, let's embark on a voyage into the seas of ownership, references, and borrowing - the core concepts that set Rust apart.
First lets understand some important concepts before diving into the topic:
Stack and Heap
Rust stores primitive datatypes directly in memory, but complex datatypes are generally stored in either the stack or the heap.
The stack stores variables whose size is known at compile time. A stack follows a last-in, first-out order. All scalar types can be stored in the stack because their size is fixed. If the size of the variable is unknown at compile time, then it is stored in the heap, which is suitable for dynamic datatypes. The heap is used to store variables that may change throughout the program's life cycle. Performing operations on the stack is faster than on the heap because of the fixed size and direct access.
Ownership: The Captain of the Ship β
What is ownership??
Rust has a unique feature called ownership that ensures memory safety and efficiency. The compiler enforces a set of rules for ownership at compile time. Different languages handle memory management in different ways. Some languages use garbage collection to periodically reclaim unused memory during runtime; others require the programmer to manually allocate and free the memory. Rust takes a different approach: it uses a system of ownership with rules that the compiler checks. The program won't compile if any of the rules are broken. Ownership doesn't affect the performance of your program at runtime.
The concept of ownership may be unfamiliar to many programmers, and it may take some time to adapt to it. The good news is that as you gain more experience with Rust and the rules of the ownership system, you will find it easier to write code that is both safe and efficient. Don't give up!
Rust Ownership Rules
Each value in Rust has a variable called its owner.
There can only be one owner at a time.
When the owner goes out of scope, the value will be dropped.
Here is a simple Rust program following the above rules
fn main() {
let mut treasure = String::from("Buried gold");
treasure.push_str(" and silver"); // Ownership transferred to treasure
println!("{}", treasure); // Prints: Buried gold and silver
} // treasure goes out of scope, cleaned up automatically
In this example, treasure
is the owner of the String, and it holds the rights to modify it. Once it goes out of scope, Rust takes care of cleaning up the String.
Link to offical Rust docs for Ownership concept
Rust programs run without memory leaks and slowness because the language automatically manages memory allocation and deallocation. This mechanism is based on ownership rules that apply to most types in Rust. However, some primitive types, such as integers, floats, and booleans, are stored on the stack and are copied instead of moved. This means they have different ownership rules. When a value's ownership is transferred from one variable to another, this is called a 'move'. To use a value after moving it, Rust offers a feature called 'cloning', which creates a copy of the value.
References and Borrowing: Deckhands at Work π€Ώ
Link to Rust's documentation for References and Borrowing
Rust's borrowing feature lets you use values without owning them. There are two kinds of references: immutable and mutable.
Immutable references give you read-only access to the value, and you can create as many as you want at the same time. Mutable references, however, let you change the value, but you can only create one mutable reference at a time for safety reasons.
When you pass values to functions, you can either move or borrow them. For data types that only live on the stack, passing a variable to a function will copy the data, while for data types that live on the heap, it will move the ownership unless you use a reference.
Reference is an address that is passed to a function as an argument, and they always start with ampersand(&).
Example:
fn main() {
let message = String::from("Land ahoy!");
print_message(&message); // Pass a reference to message
println!("{}", message); // Prints: Land ahoy!
}
fn print_message(msg: &String) {
println!("{}", msg); // Access the value through a reference
} // Reference goes out of scope, no cleanup needed
Here, print_message
borrows a reference to message
. Ownership remains with message
, and the reference is just a temporary peek at the treasure.
Mutable References: Deckhands on Duty π οΈ
Rust supports mutable references which means we can change the value it references if itβs a mutable variable.
fn main() {
let mut treasure = String::from("Dig here!");
modify_treasure(&mut treasure); // Pass a mutable reference
println!("{}", treasure); // Prints: X marks the spot!
}
fn modify_treasure(treasure: &mut String) {
treasure.push_str(" X marks the spot!"); // Modify the treasure
}
With &mut
references, you can make changes to the treasure while keeping ownership intact.
Rules of References
At any given time, you can have either one mutable reference or any number of immutable references.
References must always be valid.
Even though borrowing errors may be frustrating at times, remember that itβs the Rust compiler pointing out a potential bug early (at compile time rather than at runtime) and showing you exactly where the problem is. Then you donβt have to track down why your data isnβt what you thought it was.
Dangling References
Rust is awesome! It protects you from making a common mistake in programming languages that use pointers: dangling pointers. These are pointers that point to memory that is no longer valid, and can cause all kinds of bugs and crashes. Rust's compiler is smart enough to check that your references are always valid, and it won't let you create a dangling reference.
For example, if you try to write this code, Rust will give you a compile-time error:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Here's the error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| +++++++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
Conclusion
Rust is a language that cares deeply about memory safety and performance. It achieves this by using ownership, references, and borrowing as its core principles. These features allow Rust to check for memory errors at compile time, preventing many common bugs that plague other languages. If you want to write safe and fast code in Rust, you need to master these concepts, as they are the foundation of Rust's design.
That was alot to gulp!! The blog was quite long, but I tried to cover as much as possible with examples and explanations. If you have any questions or feedback, please leave a comment below
As we sail through the Rust seas, understanding ownership, references, and borrowing is key to avoiding the storms of memory-related issues. Join me on LinkedIn for more updates! π»πβ¨
Top comments (0)