Rust specific features

We need to cover an important topic that we've been avoiding so far: how Rust accesses and references data in memory. These features are what makes Rust interesting.

Here and there I've asked you to ignore the "*", "&" and "?" symbols. Let's have a look at them.

Pointers & References

The symbols * and & are used to work with pointers and references, which allow you to access and manipulate data stored in memory. You might have encountered references implicitly in Python when working with mutable objects like lists or dictionaries.

Rust gives you more explicit control over references and pointers.

& is called the reference operator. You use it when you want to access (also known as borrow) the value of a variable without taking ownership of it. This is useful in situations where you want to use a value in multiple places without unnecessarily copying it. We'll go over ownership a bit more later, but for now, let's have a look at a simple example:

fn main() {
    let mut numbers = [1, 2, 3, 4, 5];

    // Get a reference to the value of the third element in the list.
    println!("Before change: third_element = {}", &numbers[2]);

    // Modify the original list.
    numbers[2] = 42;

    println!("After change: third_element = {}", &numbers[2]);
}

The above example can also run without involving the reference operator. The usefulness of references becomes apparent when working with larger data structures where copying the entire structure would be inefficient. By using references, we can pass the data to a function without unnecessary copying, for example. For a more in-depth explanation, have a look at this extensive explanation on the Rust website.

* is called the dereference operator. The dereference operator is used to access the value that a reference or pointer points to. When you have a reference or a pointer, you can use the * operator to access the actual value it refers to.

fn main() {
    let x = &7;
    assert_eq!(*x, 7);
    let y = &mut 9;
    *y = 11;
    assert_eq!(*y, 11);
}

Copying & Cloning Traits

Example of Copy, which is a bitwise copy of some types that implement it natively, like scalar values, integers, floats and characters:

fn main() {
    let x: i32 = 42;
    let y = x; // `x` is copied to `y`

    println!("x: {}", x); // Both `x` and `y` can be used independently
    println!("y: {}", y);
}

The Clone trait is a more advanced Copy. This trait allows for explicit duplication of an object, with potentially more complex logic than a simple bitwise copy.

#[derive(Clone)] // This derives the `Clone` trait for our struct
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1.clone(); // Explicitly create a copy of `p1`

    println!("p1: ({}, {})", p1.x, p1.y);
    println!("p2: ({}, {})", p2.x, p2.y);
}

For simple types, it's often fine to just copy. For more complex types or custom structs, use clone when you want to create independent copies of the data.

Next, let's have a look at borrowing. We'll use that (and references) when we only need read access or mutable access to shared data.

Borrowing

In Rust, borrowing refers to the mechanism of allowing multiple references to access the same piece of data, without causing data races or memory unsafety issues. An example:

fn main() {
    let mut x = 5;
    {
        let y = &mut x; // mutable reference to x
        *y += 1;
    } // y goes out of scope, allowing x to be borrowed again
    let z = &x; // immutable reference to x
    println!("The value of x is: {}", z);
}

In Rust, we can have either one mutable reference or multiple immutable references to a piece of data at any given time. This helps prevent issues like data races and memory issues, and allows for safe concurrent programming.

Borrowing in Rust is closely related to data ownership and memory management. When a value is borrowed in Rust, it means that a reference to the value is created, rather than a new copy of the data. This reference allows the borrower to access the data without taking ownership of it.

Lifetimes

The concept of borrowing is very closely related to the concept of lifetimes which determine how long a reference to a piece of data is valid. Lifetimes help ensure that borrowed data is not used after it has been deallocated or moved, which can lead to undefined behaviour and security vulnerabilities.

It's better to see it later in the guide over some concrete examples.

In a nutshell, there are some ways in Rust to annotate some parameters to ensure that the reference to some data remains valid for the duration of a call. This is what's meant with lifetimes and is a very helpful feature in Rust.

My opinion

While these are important and very powerful features, it's good not to overdo things. We'll keep things very pragmatic going forward, all the while showing where we can optimise and how the code changes.

For now, we'll mainly use (mutable/immutable) references, a bit of lifetimes and some copying when necessary. As you become more experienced with Rust, you can further optimize your code by considering more advanced techniques which might be overkill for now.

Again, I'm not the absolute expert on this, just an every day pragmatic with a job to do. :)

There is one video on YouTube that explains these concepts very well, check it out: