Rust with diagrams: Ownership 6 - References & Borrowing

Rust with diagrams: Ownership 6 - References & Borrowing

Rust documentation with diagrams to fast learn and remember. Let's learn together in Chapter 4.2.

Before reading about References and Borrowing, you should know how variables and data interact with "move" and "clone" and understand the concept of the Heap. You can find a summary of these topics in the next articles.

Rust's memory approach, Data interacting with move & Data interaction with clone

What is a reference? Understanding Borrowing

A reference is like a pointer that it's an address can be follow to access stored at that address.

Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type of the life of that reference.

Ampersands (&) represent references, and they allow you to refer to some value without taking ownership of it. Let's see an example:

fn main() {
    let s1 = String::from("hello"); // String with pointer into Heap

    let len = calculate_length(&s1); // Here s1 reference is created without taking s1 ownership

    println!("The length of '{}' is {}.", s1, len); // Rust can access to s1 because s1 wasn't moved, instead, s1 reference was used
}

fn calculate_length(s: &String) -> usize {
    s.len()  // not need to return the values in order to give back ownership, because we never had ownership
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

We call the action of creating a reference borrowing. As in real life, if a person owns something, you can borrow it from them. When you’re done, you have to give it back. ¡You don’t own it!

Mutable & Inmutable References

By default, references are inmutables (read-only), this mean that if we try modify a reference Rust will throws an error. For example:

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world"); // Rust will throw an error in compile time
}

To fix this code, we can modify the "s" variable to mutable. To do this, we have to add a few small tweaks that use. Let's see:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world"); // Now it compiles
}

Rust avoids Data Races

With the goal of avoid "Data races" and multiple mutable references to the same data at the same time allows for mutation in a very controlled fashion, Rust has next restriction:

if you have a mutable reference to a value, you can have no other references to that value. This code that attempts to create two mutable references to s will fail

Data races cause undefined behavior and can be difficult to diagnose and fix when you're trying to track them down at run time. Data races happens when these three behaviors occur:

  • Two or more pointers access the same data at the same time.

  • At least one of the pointers is being used to write to the data.

  • There’s no mechanism being used to synchronize access to the data.

Let's see examples:

Success:

let mut s = String::from("hello");

{
    let r1 = &mut s; // no simultaneous -> no problem 
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

Error:

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);

Success:

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 point

let r3 = &mut s; // no problem
println!("{}", r3);

Rust avoids Dangling References

A Dangling Reference is a pointer that reference a location in memory that may have been given to someone else by freeing some memory while preserving a pointer to that memory. Rust compiler will ensure that the data will not go out of scope before the reference to the data does. Dangling Reference example:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

The error throwed:

error[E0106]: missing lifetime specifier

We’ll discuss lifetimes in detail later. One option to fix it is return the String directly:

fn no_dangle() -> String {
    let s = String::from("hello");

    s // Ownership is moved out, and nothing is deallocated.
}

References Rules

Let's recap what we've discussed about references:

  • At any given time, you can have either mutable reference of any number of inmutable references.

  • References must always be valid.

Next article

Once we understood References and Borrowing in Rust, we will see a different kind of reference (slices) in the next article.


I wish that this content helps you to learn Rust while i do it. If you have doubts, suggestions or you see errors, please don't be shy and comment on them. The goal of this content is learn together!

References: