References and borrowing

In the previous lesson, we had a function that gave up ownership to the other function which returned back the String and gave ownership back to the main() function.

fn main(){
  let s = String::from("Hello");
  let s = takes_ownership(s);
  println!("s is {}", s);
}
 
fn takes_ownership(s: String) -> String {
  println!("s is {}", s);
  s
}

However, this is not a good approach to move ownership from one function to another and then back to the original function, it could make your code less readable and makes using the same value in different functions at the same time impossible.

This is where references come into play, instead of a function taking String it can take it's reference &String, this way it doesn't take ownership, only borrows the value from the original variable.

fn print_str(s: &String) {
  println!("s is {}", s);
}
 
fn main(){
  let s = String::from("Hello");
 
  print_str(&s);
 
  println!("s is {}", s); // Still can use the value, because we have ownership of it
}

To borrow a value from the original value, we use the & symbol. This is called a reference.

In the example above, we borrowed the value from the original variable s and passed it to the function print_str. The function print_str borrowed the value and used it to print the value.

This reference in Rust is called Immutable Reference. This means that the value that is borrowed cannot be changed. If you try to change the value that is borrowed, you will get a compile error.

fn change_str(s: &String) { // immutable reference
    s.push_str(", some other text..."); // tries to change the value
}
 
fn main() {
    let s = String::from("Hello");
    change_str(&s);
    println!("From main: {}", s);
}

Mutating an immutable reference Mutating an immutable reference

Immutable references can not modify or mutate the original value, this is a safety feature in Rust to prevent data inconsistency and bugs that would later be hard to debug. Rust gives another way to change the value that is borrowed, and that is by using mutable references.

Mutable references

This is where mutable references come in to play. If you want to change the value that is borrowed, you need to use a mutable reference. You can create a mutable reference by using &mut instead of &.

fn change_str(s: &mut String) {
    s.push_str(", some other text...");
}
 
fn main() {
    let mut s = String::from("Hello");
    change_str(&mut s);
    println!("From main: {}", s);
}

The code now works, notice that we changed a few things in the code.

  1. We changed the function argument to be a mutable reference change_str(s: &mut String).
  2. We changed the variable assignment to be mutable let mut s = String::from("Hello");.
  3. When passing the variable to the change_str function, we also changed the reference to be mutable change_str(&mut s);.

Specifying the references as mutable or immutable is a core safety feature that Rust provides.

When it comes to concurrency, mutable references can be very tricky, because if you mutate the value in many places at the same time, you can get into a situation where the value is not what you are expecting, Rust enforces these rules at compile time to prevent such situations, and provides other efficient mechanisms to handle mutable references in concurrent environments which we will cover in the next lessons.

For immutable references, the compiler can guarantee that the value will not be changed, and for mutable references we'll need to implement specific mechanisms to synchronize the mutable access to the value otherwise the compiler will give an error and the code will not compile.

Mutable references limitations

Using immutable references is the preferred way to borrow values in Rust, because it's safer and the compiler can guarantee that the value will not be changed, you can also have many immutable access to the same value at the same time.

However, when it comes to mutable references, it's a little bit different.

Mutable references have limitations, if you are declaring a variable as mutable, then you can only have one mutable reference to a value in a scope, this is another Ownership rule that we need to follow.

Don't worry if what was just said didn't make sense to you, we'll explain it in more detail.

Here's an example

Let's bring an imaginary example to illustrate this. In real-life apps you will never need to write code like this, but it's a good example to illustrate the point.

Let's say we are having a function that takes two mutable references to a String and concatenates them together, and then returns the concatenated value.

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &mut s;
    let r2 = &mut s;
 
    concatenate(r1, r2);
}
 
fn concatenate(s1: &mut String, s2: &mut String) -> String {
    let result = s1.to_string() + s2;
    result
}

At first, the code looks fine and every type is correct. the concatenate function takes two mutable references to a String and we've provided exactly that.

However, when you try to compile the code, you will get an error.

Multiple mutable references Multiple mutable references

This all happens when multiple mutable references are being used at the same time, if you are doing the same with immutable references, the code will compile and run successfully, this is because the Rust compiler can guarantee that the value will not be changed.

fn main() {
    let s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
 
    println!("r1: {}, r2: {}", r1, r2);
}

Multiple immutable references Multiple immutable references

The reason this error happens is that the Rust compiler can not guarantee that the two mutable references will not change the value of the variable s at the same time, and if that happens that means your reference is pointing to a value that is not what you are expecting, this can cause data inconsistency and bugs in programming languages that do not have this safety feature, this is called a Data race.

Data races

A data race happens when one of these happen:

  • Two or more pointers access the same data at the same time.
  • At least pointer is mutating the data.
  • There's no mechanism being used to synchronize the data.

In the previous example, if we have an r3 with the type &mut s, the code will not compile and you will get an error.

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
 
    println!("r1: {}, r2: {}, r3: {}", r1, r2, r3);
}

This is because we are using the mutable reference and the immutable references at the same time, and the Rust compiler can not guarantee that the value will not be changed, so it gives an error.

But if we remove the println! statement, the code will compile and run successfully, this might be a little bit strange, because the value is still borrowed as mutable, but the compiler seems to be happy.

The reason this code can compile is that the Rust compiler can guarantee that the value will not be changed, because it hasn't been used up to that point.

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
}

The compiler successfully compiles the code and runs it, the error only happens when the compiler can not guarantee that the immutable references do not change.

That means, if we use the immutable references first and then mutating the value, the compiler can still guarantee that the immutable references will not change and point to the data that we are expecting.

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;
    let r2 = &s;
 
    println!("r1: {}, r2: {}", r1, r2);
 
    // Mutable borrow later is fine because the
    // immutable references are not used after this point
    let r3 = &mut s;
 
    println!("r3: {}", r3);
}

Another way to structure the code above is to mutate the value first and then use the immutable references, this also gives the guarantee that we are not changing the value that the immutable references are pointing to.

fn main() {
    let mut s = String::from("hello");
 
    let r3 = &mut s;
    r3.push_str(", world");
 
    let r1 = &s;
    let r2 = &s;
 
    println!("r1: {}, r2: {}", r1, r2);
}

With mutable references, you can not use multiple mutable references at the same time, and you can not use mutable references with immutable references at the same time, this is a core safety feature in Rust to ensure that the value is not changed unexpectedly.


In the next lesson, we're going to explore Strings and Slices in Rust. A slice is a reference to a part of a value in a collection like a String, which is a collection of UTF-8 encoded bytes.

We'll see how slices work and how they are used in Rust, which is a great way to explain the ownership system of Rust.

Rustfinity.com

Links

  1. Home
  2. Learn Rust
  3. Get Started
  4. Practice Rust
  5. Challenges
  6. Tutorials
  7. Blog
  8. Open source
  9. Learn Gleam

Socials

  1. GitHub
  2. X

Legal

  1. Privacy Policy
  2. Terms of Service