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.
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.
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.
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 &
.
The code now works, notice that we changed a few things in the code.
- We changed the function argument to be a mutable reference
change_str(s: &mut String)
. - We changed the variable assignment to be mutable
let mut s = String::from("Hello");
. - When passing the variable to the
change_str
function, we also changed the reference to be mutablechange_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.
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
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.
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.
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.
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.
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.
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.