In the previous chapter, we saw how ownership works in Rust. The main drawback we saw was that for functions, the return value changes ownership and the original variable is no longer usable. In this chapter, we will look at a continuation of ownership, i.e. references.
References
Instead of passing variables to functions, we can pass references
to functions. reference
is like a pointer
we can follow to get to the data. Unlike a pointer, a reference
is guaranteed to point to valid data for the
life of that reference.
In rust
, references can be used as follows:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len); // here s is valid as we only pass its reference
}
fn calculate_length(s: &String) -> usize { // we need to pass the type of the reference
s.len()
}
In the above function, passing the ampersand String
instead of String
is called referencing
and it allows us to
refer to a value without taking ownership of it. Since the value s
in the function is only referenced and not owned,
it will not be dropped when the function ends, and would thus still be valid.
This action of creating a reference and passing it to a function that accepts a reference is called borrowing
, since
we are only borrowing a value and not taking ownership
of it! Neat!!!!
Modifying borrowed values
What were to happen if we try to modify a value we have borrowed? For example, in the function below, we try to
modify the reference
that is passed to the function:
fn main() {
let s = String::from("hello");
update_string(&s);
fn update_string(s: &String) {
s.push_str(", world!");
}
}
If we try to run the above code, we will get the following error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error
The error is pretty self-explanatory. We cannot modify a value that we have borrowed. This is because when we pass a
reference to a function, we are only allowed to read the value, not modify it. This is called immutable
borrowing.
This is also the same behaviour that we have seen for variables that are immutable
by default in Chapter 3.
Mutable References
Just as we had mutable variables, we can also have mutable references. We can create a mutable reference by using
&mut
instead of &
:
fn main() {
let mut s = String::from("hello");
update_string(&mut s);
fn update_string(s: &mut String) {
s.push_str(", world!");
}
}
In the above implementation, we pass the &mut s
to the function as reference instead of &s
.
Next, we also pass the &mut String
to the function instead of &String
, implying that we are passing a mutable
reference to the function.
Major restriction
: We can only have one mutable reference to a particular piece of data in a particular scope.
In short, there can only be a single mutable reference
to a particular variable that we can use.
For example, the following code will not compile:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
If we run the above code, it will raise the following error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error
Since we are trying to create two references r1
and r2
to the same variable s
, the compiler will throw an error.
The error is because we are borrowing the same variable a second time before the scope of the first variable
is not finished.
If we were to change the above code to the following, it will run without this error:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
println!("The value of r1 is {r1}");
let r2 = &mut s;
println!("The value of r1 is {r2}");
}
In this case, we are first ending the scope of r1
when we call print, and we are then creating a second
reference r2
to the variable s
. This is allowed by the compiler.
Resticting multiple mutable references
: The main advantage for restricting multiple mutable references
is that the compiler does not have data races at compile time. A data race is similar to a race condition and 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.
We can also use {}
to scope a mutable reference such that there is no conflict between two mutable references:
fn main() {
let mut s = String::from("Hello");
{
let r1 = &mut s;
println!("The value of r1 is {}", r1);
} // scope of mutable reference is finished, and a new variable with mutable reference can be created
let r2 = &mut s;
println!("The value of r2 is {}", r2);
}
The above code will also compile, since the scope of the first mutable reference r1
is finished before we create
the second mutable reference r2
by using {}
.
Scope matters for mutable and immutable references!
We can have multiple immutable
references from a variable, since by nature, immutable references cannot
change any value, and can be looked at as a read-only
reference. However, we cannot put an mutable
reference
before the scope
of the immutable
reference is finished, since the mutable
reference can change the value
of the variable, and thus, the immutable
reference will not be valid anymore.
This can be seen clearly from the below example:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem, since immutable reference used
let r2 = &s; // no problem, since immutable reference used
let r3 = &mut s; // BIG PROBLEM, mutable reference used before immutable references are finished
println!("{}, {}, and {}", r1, r2, r3); // will not compile
}
Running the above code will raise the following error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, and {}", r1, r2, r3);
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
As we can see, the compiler will not allow us to create a mutable reference r3
before the immutable references
r1
and r2
are finished. This is because the mutable reference r3
can change the value of the variable s
,
and thus, the immutable references r1
and r2
will not be valid anymore.
However, the below code is valid and will compile:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem, read-only
let r2 = &s; // no problem, read-only
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point
let r3 = &mut s; // no problem even if modified
println!("{}", r3);
}
Dangling references
For languages with pointers, it is possible that a reference can point to a location that is given to another
variable, or free some other variables memory by mistake. In rust
, the compiler never lets us create dangling references
to a variable. If we have a reference
to some data, the compiler will make sure that the data
does not go out of scope before the reference is out of scope.
Let’s look at the below example to see how rust
prevents us creating a dangling reference.
fn main() {
let some_reference = dangle(); // is a reference to a return value of a function
fn dangle() -> &String {
let s = String::from("hello"); // s comes into scope at this line
&s // create a reference to s and return it
} // the function finishes and s goes out of scope after this line, so the reference has nothing to
// point to
}
Running the above, we get the following error:
error[E0106]: missing lifetime specifier
--> src/main.rs:6:20
|
6 | fn dangle() -> &String {
| ^ 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
|
6 | fn dangle() -> &'static String {
| +++++++
The error says that this functions's return type contains a borrowed value, but there is no value for it to be borrowed from
. This is because the variable s
goes out of scope after the function dangle
finishes, and
thus, the reference &s
has nothing to point to.
Instead of returning a reference in the above function, we need to return s
directly, and take ownership of
the variable s
in the function dangle
:
We can see how a plan comes together in rust
with ownership and scope.
In the next chapter, we will look at a different kind of reference, called slice
.