DEV Community

Cover image for Rust's Borrowing and Reference Laws
Itachi Uchiha
Itachi Uchiha

Posted on

Rust's Borrowing and Reference Laws

This post published on my blog before


Hi everyone. Before that, I wrote a post called Ownership Concept in Rust.

Today I'll try to explain some of the features called borrowing and references. I called them laws in the title.

Before starting, I’ll create a project with cargo;

cargo new references_and_borrowing

cd references_and_borrowing
Enter fullscreen mode Exit fullscreen mode

Introduction

We've discussed the ownership concept in the last post. When you assign a heap-allocated variable to another one, we also transfer its ownership, and the first variable we created is no longer used. This explains why Rust is a secure programming language. Rust's memory management mechanism is different than others.

For example, when you passed a variable to a function as a parameter, that variable is no longer used. It looks like annoying. You don't want to always create new variables to do the same stuff.

Sometimes you don't want to move variable's ownership but you also want to its data. In these kinds of cases, you can use the borrowing mechanism. Instead of passing a variable as a value, you send a variable as a reference.

References hold memory addresses of variables. They don't hold the data of variables.

What are Benefits?

Benefits

Image from Freepik

The compiler always checks the reference object statically. It doing this with a mechanism called borrow checker. So, it prevents to delete object while it has a reference.

Let's See Examples

The first example will be like that;

fn main() {
  let my_name = String::from("Ali");

  //this ampersand is a reference
  let len_name = name_length(&my_name);
  //  -----------------------^
}

fn name_length(string: &String) -> usize {
    // ----------------^
    string.len()
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we've passed a variable as a reference. When you use & ampersand symbol, it's transforming to a reference. The key point is here if we pass a reference variable to a function as a parameter, that function will accept a reference parameter. If you don't use an ampersand you'll see a compile-time error like that;

 --> src/main.rs:5:32
  |
5 |     let len_name = name_length(&my_name);
  |                                ^^^^^^^^
  |                                |
  |                                expected struct `std::string::String`, found `&std::string::String`
  |                                help: consider removing the borrow: `my_name`
Enter fullscreen mode Exit fullscreen mode

Our second example will be like that;

fn main() {
  let boxed_integer = Box::new(5);
  let reference_integer = &boxed_integer;

  println!("Boxed: {} and Reference {}", boxed_integer, reference_integer);
}
Enter fullscreen mode Exit fullscreen mode

As you see, when we send or assign a parameter, our assigned or passed values starts with an ampersand. What would happens if we didn't use this ampersand? It won't work. Because when we assigned boxed_integer to reference_integer, compiler removed the boxed_integer variable.

A Problem About Removed Resource

Problem

Image from Freepik

Let's think of a scenario. We know that when references are available, the resource won't be removed. What will happen if the resource removed before the reference? Let me say the reference will point to a null unit or random unit. This is a really big problem here. But don't worry about it. The Rust avoids this problem thanks to the compiler. Let's see an example.

fn a_function(box_variable: Box<u32>) {
    println!("Box Variable {}", box_variable);
}

fn main() {
    let box_variable = Box::new(6);
    let reference_box_var = box_variable;

    a_function(box_variable);

}
Enter fullscreen mode Exit fullscreen mode

We'll see an error here;

error[E0382]: use of moved value: `box_variable`
 --> src/main.rs:9:16
  |
6 |     let box_variable = Box::new(6);
  |         ------------ move occurs because `box_variable` has type `std::boxed::Box<u32>`, which does not implement the `Copy` trait
7 |     let reference_box_var = box_variable;
  |                             ------------ value moved here
8 |     
9 |     a_function(box_variable);
  |                ^^^^^^^^^^^^ value used here after move

error: aborting due to previous error; 1 warning emitted
Enter fullscreen mode Exit fullscreen mode

To solve this problem, we could create a new scope;

fn a_function(box_variable: Box<u32>) {
    println!("Box Variable {}", box_variable);
}

fn main() {
    let box_variable = Box::new(6);

    // a scope
    {
        let reference_box_var = &box_variable;
    }

    a_function(box_variable);

}
Enter fullscreen mode Exit fullscreen mode

Mutable Borrow

Until now, we always see immutable borrow examples. But sometimes you will need mutable borrows. We only read values from immutable borrow resources. We didn't change reference's value.

Normally when we accept reference we use this notation &T but if we want to mutable reference we should use &mut T. However, we can't create a mutable reference while the variable is immutable. Let's see an example

fn main() {
    let mut a_number = 3;

    {
        let a_scoped_variable = &mut a_number;
        *a_scoped_variable +=1;
    }

    println!("A number is {}", a_number); // A number is 4

}
Enter fullscreen mode Exit fullscreen mode
let a_scoped_variable = &mut a_number;
*a_scoped_variable +=1;
Enter fullscreen mode Exit fullscreen mode

In the above example, if we didn't use mut the following line won't work. And if we didn't create a variable with the mut keyword at the beginning, the compiler will throw an error. Because we were trying to get a mutable reference from an immutable variable.

There is also a key point we should know that you can have only one mutable reference. So you can't create multiple mutable references. I give an example that will fail.

fn main() {
    let mut number = Box::new(5);

    let number2 = &mut number;
    let number3 = &mut number;

    println!("{}, {}", number2, number3);
}
Enter fullscreen mode Exit fullscreen mode

It will show an error like that;

Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `number` as mutable more than once at a time
 --> src/main.rs:5:19
  |
4 |     let number2 = &mut number;
  |                   ----------- first mutable borrow occurs here
5 |     let number3 = &mut number;
  |                   ^^^^^^^^^^^ second mutable borrow occurs here
6 |     
7 |     println!("{}, {}", number2, number3);
  |                        ------- first borrow later used here

Enter fullscreen mode Exit fullscreen mode

The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:

1-) Two or more pointers access the same data at the same time.
2-) At least one of the pointers is being used to write to the data.
3-) There’s no mechanism being used to synchronize access to the data.
Enter fullscreen mode Exit fullscreen mode

We can fix this error by creating a new scope;

fn main() {
    let mut number = Box::new(5);

    let number2 = &mut number;

    {
        let number3 = &mut number;
    }

}
Enter fullscreen mode Exit fullscreen mode

There is a similar rule like this rule when you're combining mutable and immutable references.

fn main() {
    let mut number = Box::new(5);

    let number2 = &number;
    let number3 = &mut number;

    println!("Number2 {} and Number3 {}", number2, number3);

}
Enter fullscreen mode Exit fullscreen mode

This code piece won't work. You'll get an error;

Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `number` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:19
  |
4 |     let number2 = &number;
  |                   ------- immutable borrow occurs here
5 |     let number3 = &mut number;
  |                   ^^^^^^^^^^^ mutable borrow occurs here
6 |     
7 |     println!("Number2 {} and Number3 {}", number2, number3);
  |                                           ------- immutable borrow later used here
Enter fullscreen mode Exit fullscreen mode

We can fix this code by this way;

fn main() {
    let mut number = Box::new(5);

    let number2 = &number;

    println!("Number2 {}", number2);

    let number3 = &mut number;

    println!("Number3 {}", number3);
}
Enter fullscreen mode Exit fullscreen mode

It worked. Because a reference’s scope starts from where it is introduced and continues through the last time that reference is used.

Dangling References

In languages which have pointers, you can create dangling pointers easily. For example, this problem occurs in C programming language like that;

int *afunc();
void main()
{
  int *pointer;
  pointer = afunc();

  fflush(stdin);
  printf("%d", *pointer);
}

int * afunc()
{
  int x = 1000;
  ++x;

  return &x;
}
Enter fullscreen mode Exit fullscreen mode

It should throw an output like that;

warning: function returns the address of the local variable

So, what's happening in Rust programming language? The compiler guarantees that references will never be dangling references. For example, you wrote code like the above code, the compiler, won't compile your code. It will ensure that the data will not go out of scope before the reference to the data does.

Let's try to create a dangling reference and we'll sure Rust will prevent to compile your code.

fn main() {
    let dangling_reference = a_func();
}

fn a_func() -> &Box<u8> {
    let x = Box::new(10);

    &x
}
Enter fullscreen mode Exit fullscreen mode

We'll see an error at compile-time;

Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn a_func() -> &Box<u8> {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn a_func() -> &'static Box<u8> {
  |                ^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

As you see, our code didn't compile. The solution is to return your data directly. Don't use ampersand.

fn main() {
    let dangling_reference = a_func();
}

fn a_func() -> Box<u8> {
    let x = Box::new(10);

    x
}
Enter fullscreen mode Exit fullscreen mode

What Did We Learn?

What Did We Learn

Image from Freepik

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • References must always be valid.

That’s all for now. If there is something wrong, please let me know.

Thanks for reading.

Resources

Top comments (1)

Collapse
 
biomathcode profile image
Pratik sharma

hey, I am just starting to learn rust and this was very helpful.
Borrowing and Ownership are the main features of rust I would say. Which makes Rust quite different from other languages.