DEV Community

Cover image for Day 9: Navigating Ownership, References, and Borrowing in Rust 🚒
Aniket Botre
Aniket Botre

Posted on

Day 9: Navigating Ownership, References, and Borrowing in Rust 🚒

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
Enter fullscreen mode Exit fullscreen mode

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.

Meme


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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Meme

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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! πŸ’»πŸŒŠβœ¨

RustLang #Programming #LearningToCode #CodeNewbie #TechJourney #Day9

Top comments (0)