References and Borrowing in Rust

francescoxx

Francesco Ciulla

Posted on February 27, 2024

References and Borrowing in Rust

References and Borrowing

In this article, we will see how to use references in Rust. If you want an introduction to Ownership in Rust, in can check the previous article

In this lesson, we will see:

  • Ownership and Functions
  • Return Values and Scope
  • Introduction to References and Borrowing
  • Mutable References
  • Multiple Mutable References
  • Mutable and Immutable References
  • Dangling references
  • References Rules

If you prefer a video version

All the code is available on GitHub (link available in the video description)

Ownership and Functions

When you pass a variable to a function, you move or copy it.

If you move it, as it happens with strings, the variable is no longer valid after the function call.



//ownership and functions
fn main(){
    let i = 5;
    call_integer(i);
    println!("{}", i);

    let s = String::from ("Hello, World!");
    call_string(s);
    println!("{}", s);
}


Enter fullscreen mode Exit fullscreen mode

References and borrowing

Return Values and Scope

When a function returns a value, it gives ownership of the value to the calling function.

We can also have a function that has ownership but gives it back to the calling function.



//ownership and functions
fn main(){
    let s1 = give_ownership();
    let s2 = String::from("hello");  // s2 comes into scope
    let s3 = take_and_give_back(s2); // s2 is moved into take_and_give_back, it returns s3

    println!("s1: {}", s1);
    println!("s2: {}", s2); // error: value used here after move
    println!("s3: {}", s3);
}

fn give_ownership() -> String {
    let some_string = String::from("hello");
    some_string
}

fn take_and_give_back(a_string: String) -> String {
    a_string
}


Enter fullscreen mode Exit fullscreen mode

Let's type cargo run -q and see the output:

References and borrowing

But if we try to use the s2 variable after the function call, we get an error.

References and borrowing

So how can we solve this? Let's see an example.

A first example of getting the ownership back

One first solution might be this: we can return the value passed in input back, using a tuple.

For example, if we want to calculate the length of a string, we can return the string and its length.



fn main(){
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}", s2, len);
}

fn calculate_length(s: String) -> (String, usize){
    let length = s.len();
    (s, length)
}


Enter fullscreen mode Exit fullscreen mode

This "works", but it's not the best solution.

References and borrowing

This solution is tedious and error-prone.

Rust has a feature called references that allows you to refer to some value without taking ownership of it.

Let's see how to modify the previous example using references.

Introduction to References and Borrowing

Modify the code above to use references instead of taking ownership of the string.



fn main(){
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}", s1, len);
}

fn calculate_length(s: &String) -> usize{
    let length = s.len();
    length
}


Enter fullscreen mode Exit fullscreen mode

Explanation: &s1 creates a reference to the value of s1 but does not take ownership of it.

In the signature of calculate_length, s: &String means that s is a reference to a String.

If we type cargo run -q, we get the expected output:

References and borrowing

We are getting the same outut as before, but now we are not taking ownership of the string.

Here is a schema of what happens when we pass a reference to a function:

References and borrowing

IMPORTANT: The action of passing a reference to a function is called Borrowing.

Mutable References

Can we change the value of a reference?



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

fn change(s: &String){
    s.push_str(", world");
}



Enter fullscreen mode Exit fullscreen mode

But here we get an error:

References and borrowing

References are immutable by default.

But we can make them mutable by using &mut instead of &.

Of course, the value of the variable must be mutable as well.



//mutable references
fn main(){
    let mut s = String::from("hello");
    modify(&mut s);
    println!("{}", s);
}

fn modify(s: &mut String){
    s.push_str(" world");
}


Enter fullscreen mode Exit fullscreen mode

Multiple Mutable References

Can we have multiple mutable references to the same variable?



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

    let s1 = &mut s;
    let s2 = &mut s;

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


Enter fullscreen mode Exit fullscreen mode

This code is not valid.

References and borrowing

This should surprise you. In many programming languages, having multiple references to the same variable is not a problem. But this can lead to problems at runtime.

In Rust, the compiler prevents this from happening.

Of course, we can have multiple mutable references, but in different scopes.



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

    {
        let s1 = &mut s;
        s1.push_str(" world");
    }   //s1 goes out of scope here

    let s2 = &mut s;
    s2.push_str("!");

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


Enter fullscreen mode Exit fullscreen mode

This code is valid.

Mutable and Immutable References

Let's try to mix mutable and immutable references.



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

    let s1 = &s; //immutable borrow
    let s2 = &s; //immutable borrow

    let s3 = &mut s; //mutable borrow

    println!("{}, {}, {}", s1, s2, s3);
    //throws error: cannot borrow `s` as mutable because it is also borrowed as immutable
}


Enter fullscreen mode Exit fullscreen mode

This code is not valid: we cannot have a mutable reference while we have immutable references.

References and borrowing

But if we use the immutable references before the mutable reference, the code is valid.



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

    let s1 = &s; //immutable borrow
    let s2 = &s; //immutable borrow

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

    let s3 = &mut s; //mutable borrow

    println!("{}", s3);
}


Enter fullscreen mode Exit fullscreen mode

This code is valid because the immutable borrows are not used after the mutable borrow.

Dangling references

A Dangling reference is a reference that points to an invalid memory.



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

fn dangle() -> &String {
    let s = String::from("hello");
    &s
}


Enter fullscreen mode Exit fullscreen mode

In this case, the reference &s is returned, but s goes out of scope at the end of the function.

References and borrowing

To solve this problem, we can return the string directly.



fn main() {
    let s = no_dangle();
    println!("{}", s);
}

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


Enter fullscreen mode Exit fullscreen mode

References and borrowing

This code is valid.

References Rules

There are 2 main rules for references:

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

  • References must always be valid.

References and borrowing

Conclusion

In this article, we have seen how to use references in Rust.

We have seen how to pass references to functions, how to use mutable references, and the rules for using references.

If you prefer a video version

All the code is available on GitHub (link available in the video description)

You can find me here: Francesco

💖 💪 🙅 🚩
francescoxx
Francesco Ciulla

Posted on February 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

References and Borrowing in Rust
webdev References and Borrowing in Rust

February 27, 2024