Hey Pixelers,

Michael Haddon Here,

Rust is deceptive with its difficulty, once you understand a few concepts the whole language is so much easier to use.

You may be fighting with your code,

fn main() {
    let s1 = String::from("Hello, Rust!");
    let s2 = s1;

    println!("s1: {}", s1);
}

(println!(“s1: {}”, s1);) Gives the error:

borrow of moved value: `s1` [E0382] Help: consider cloning the value if the performance cost is acceptable

or

fn main() {
    let operation_types: Vec<i32> = vec![0, 1];
    let s1: String = "Hello World".to_string();

    if operation_types.contains(&0) {
        run_operation(0, s1);
    }
    if operation_types.contains(&1) {
        run_operation(1, s1);
    }
}

fn run_operation(operation_type: i32, input: String) {
    println!("Operation type: {}, Input: {}", operation_type, input);
}
×
fn main() {
    let operation_types: Vec<i32> = vec![0, 1];
    let s1: String = "Hello World".to_string();

    if operation_types.contains(&0) {
        run_operation(0, s1);
    }
    if operation_types.contains(&1) {
        run_operation(1, s1);
    }
}

fn run_operation(operation_type: i32, input: String) {
    println!("Operation type: {}, Input: {}", operation_type, input);
}

second input; gives the error:

Use of moved value 

Both of these code examples seem like they should execute normally, and they would, in other languages.
However, rust has specific intentional quirks, that you will love if you understand the purpose.

Let’s dive right in.


# Understanding Ownership

First things first, let’s talk about ownership.
Rust’s ownership system helps ensure memory safety without the need for garbage collection.
This leads to more efficient and performant code.
Remember these three rules of ownership:

  • Each value in Rust has a single owner.
  • When the owner goes out of scope, the value is automatically dropped.
  • Only one mutable reference or multiple immutable references can exist for a given value.

Got it? Great! Now, let’s move on to borrowing.


# Borrowing & References

Borrowing is all about letting your code access data without taking ownership.
There are two types of borrowing in Rust: mutable and immutable.
When you create a reference to a value, you’re borrowing it.
The trick is to remember that mutable references must be unique, while you can have multiple immutable references.
Let’s start with an example of creating immutable and mutable references.

fn main() {
    let mut s1 = String::from("Hello, Rust!");

    let immutable_reference = &s1;
    println!("Immutable reference: {}", immutable_reference);

    let mutable_reference = &mut s1;
    mutable_reference.push_str(" Let's dive into ownership and borrowing!");
    println!("Mutable reference: {}", mutable_reference);
}
×
fn main() {
    let mut s1 = String::from("Hello, Rust!");

    let immutable_reference = &s1;
    println!("Immutable reference: {}", immutable_reference);

    let mutable_reference = &mut s1;
    mutable_reference.push_str(" Let's dive into ownership and borrowing!");
    println!("Mutable reference: {}", mutable_reference);
}

Notice how we use the ampersand (&) to create references.
Immutable references allow read-only access, while mutable references, denoted by ‘&mut’, allow modification.


# Lifetimes

Now, we’re going to explore lifetimes, a crucial aspect of Rust’s ownership and borrowing system.
Lifetimes help Rust ensure that references are valid for as long as they’re in use.
By explicitly annotating lifetimes, you give the Rust compiler the necessary information to check for dangling references.
Let’s implement a function that returns the longer of two string slices.

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rustaceans");

    let result = longest(&s1, &s2);
    println!("The longest string is: {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
×
fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rustaceans");

    let result = longest(&s1, &s2);
    println!("The longest string is: {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

In this example, we define a lifetime parameter 'a and use it to annotate the input string slice references and the output.
This tells Rust that the returned reference will have the same lifetime as the shorter of the input lifetimes, ensuring no dangling references occur.


# Cloning

Sometimes you may need to create a new, independent copy of a value while still keeping the original value intact.
That’s where Rust’s clone method comes in handy.
It is also a bit of a cheat tool that you can use to avoid all these weird, pesky rust benefits.
Let’s take a look at an example that demonstrates the use of clone to avoid ownership issues.

fn main() {
    let s1 = String::from("Hello, Rust!");
    let s2 = s1.clone();

    println!("s1: {}", s1);
    println!("s2: {}", s2);
}

Instead of transferring ownership from s1 to s2, we create a new, separate copy of s1’s value using the clone method.
This way, both s1 and s2 are valid and can be used independently of each other, without triggering any ownership errors.
Keep in mind that cloning might have performance implications, as it creates a deep copy of the value.
Therefore, it’s essential to use it judiciously and only when necessary to avoid potential performance issues.


# Shared Ownership

Now let’s talk about shared ownership with Rc and Arc.
These two smart pointers allow multiple parts of your code to share ownership of a value, and they automatically clean up the memory once all owners are done using it.
Rc stands for ‘Reference Counting’ and is used in single-threaded scenarios, while Arc, short for ‘Atomic Reference Counting’, is suitable for multi-threaded scenarios.
Let’s explore a quick example using Rc.

use std::rc::Rc;

fn main() {
    let s1 = Rc::new(String::from("Hello, Rust!"));
    let s2 = Rc::clone(&s1);
    let s3 = Rc::clone(&s1);

    println!("s1: {}", s1);
    println!("s2: {}", s2);
    println!("s3: {}", s3);
}
×
use std::rc::Rc;

fn main() {
    let s1 = Rc::new(String::from("Hello, Rust!"));
    let s2 = Rc::clone(&s1);
    let s3 = Rc::clone(&s1);

    println!("s1: {}", s1);
    println!("s2: {}", s2);
    println!("s3: {}", s3);
}

We create an Rc<String> called s1.
We then create two new Rc<String> instances, s2 and s3, by cloning s1.
The Rc smart pointer takes care of reference counting, ensuring that the memory is only deallocated once all the Rc instances are dropped.
The usage of Arc is similar to Rc, but it’s designed for multi-threaded situations.

Only use these though when absolutely necessary.
Rc and Arc introduce some overhead due to reference counting, so it’s best to use them only when you genuinely need shared ownership.
In most cases, Rust’s ownership and borrowing system is sufficient to manage resources.


# Tips & Tricks

Before we wrap up, let’s go through some tips and tricks to help you master Rust’s ownership and borrowing system:

  1. Keep functions small and focused to avoid lifetime conflicts.
  2. Use Rust’s smart pointers like Rc and Arc for shared ownership.
  3. Make use of the ‘clone’ method when you need to create a deep copy of a value.

Keep practicing, and you’ll be a Rust ownership and borrowing pro in no time!

And that’s a wrap!
Thanks for joining us in this deep dive into Rust’s ownership and borrowing system.
I hope you’ve learned a ton and are ready to apply these concepts to your Rust projects.

Thanks for checking out this video,
As always, hope you guys have a lovely day
Cheers