Even if it's sometimes hidden, Rust differs between values (of a certain type T
), shared references (to a value of that type T
), and mutable references (to a value of that type T
).
The std::io::Stdin::read_line
method does not retrieve a String
but a mutable reference to a String
. So passing guess
there would be a type error.
Distinguishing between passing by-value or passing shared or mutable references helps keeping track of which part of the program may modify which data. Ultimately, this is very helpful for concurrency to avoid situations where one thread modifies data that is accessed by another. But even in the single-threaded case, this can have advantages. Consider the following example:
#[derive(Copy, Clone, Debug)]
enum Fruit {
Apple,
Pear,
Banana,
}
use Fruit::*;
fn get_last_element1(list: &[Fruit]) -> Fruit {
if list.len() > 0 {
list[list.len() - 1]
} else {
panic!("empty list of fruits; can't get last element")
}
}
fn get_last_element2(list: &mut Vec<Fruit>) -> Fruit {
list.pop()
.expect("empty list of fruits; can't get last element")
}
fn main() {
// The following `mut` makes us aware that the list may change,
// e.g. it may be different for Block A and Block C:
let mut list = vec![Apple, Pear, Banana];
// Let's try to hold a reference to the last element:
let last_fruit: &Fruit = &list[list.len() - 2];
{
// Block A
// Passing a shared reference here means that the list cannot
// be modified, i.e. the full list will still be the full list.
println!("Last element: {:?}", get_last_element1(&list));
println!("Full list: {:?}", list);
}
println!("---");
{
// Block B
// Passing a mutable reference here means that we must expect
// the list to be changed.
println!("Last element: {:?}", get_last_element2(&mut list));
println!("Full list: {:?}", list);
}
println!("---");
{
// Block C
// The list has been changed by Block B already.
println!("Last element: {:?}", get_last_element1(&list));
println!("Full list: {:?}", list);
}
println!("---");
// What should `last_fruit` refer to? The banana has already been
// removed from the list.
println!("Inspecting reference: {:?}", last_fruit);
}
(Playground)
get_last_element1
and get_last_element2
are two fundamentally different functions. The first one returns a copy of the last element without modifying the list, and the second removes the last element from the list. By passing references of two different types helps making this difference clear. Thus you can't accidentally write get_last_element2(&list)
instead of get_last_element1(&list)
.
Requiring bindings to be declared with mut
(such as in let mut list = vec![Apple, Pear, Banana];
) helps making clear that we are allowed to create mutable references to the list (and that what is list
in this scope can change throughout this scope).
Let's compare this with an example in Python:
#!/usr/bin/env python3
def get_last_element1(fruits):
return fruits[-1]
def get_last_element2(fruits):
return fruits.pop()
fruitsA = ("Apple", "Pear", "Banana") # non-mutable tuple
get_last_element1(fruitsA)
#get_last_element2(fruitsA) # this would cause an error
fruitsB = ["Apple", "Pear", "Banana"] # mutable list
print(get_last_element1(fruitsB)) # does this modify `fruitlist`?
print(get_last_element2(fruitsB)) # does this modify `fruitlist`?
Python3 allows us to create a list of fruits as an immutable tuple fruitsA
or mutable list fruitsB
. Thus, if we write get_last_element2(fruitsA)
, we will get a runtime error, because get_last_element2
will try to modify the tuple (which doesn't work).
But when we later write these two lines:
print(get_last_element1(fruitsB)) # does this modify `fruitlist`?
print(get_last_element2(fruitsB)) # does this modify `fruitlist`?
Then it's very hard to tell which of these two lines actually modify fruitsB
here.
Another example in Python:
#!/usr/bin/env python3
fruitsalads = [["Apple", "Pear"], ["Apple", "Banana"]]
# Let's try to make fruitsalads immutable:
fruitsalads = tuple(fruitsalads)
#fruitsalads.pop() # this would cause an error now
fruitsalads[0].pop() # but we can still mutate the contained first list
print(fruitsalads)
This will print:
(['Apple'], ['Apple', 'Banana'])
Despite our attempt to make fruitsalads
immutable.
In Rust, we'd be protected from these problems, because if we only have a shared reference to the outer Vec
, we cannot use that to get a mutable reference to the inner Vec
:
fn try_to_modify(vec: &Vec<Vec<i32>>) {
vec[0].pop();
}
fn main() {
let mut vec_of_vecs = vec![vec![1, 2], vec![3, 4]];
vec_of_vecs.push(vec![5, 6]);
try_to_modify(&vec_of_vecs);
}
(Playground)
Errors:
Compiling playground v0.0.1 (/playground)
error[E0596]: cannot borrow `*vec` as mutable, as it is behind a `&` reference
--> src/main.rs:2:5
|
1 | fn try_to_modify(vec: &Vec<Vec<i32>>) {
| -------------- help: consider changing this to be a mutable reference: `&mut Vec<Vec<i32>>`
2 | vec[0].pop();
| ^^^ `vec` 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 `playground` due to previous error
So Rust distinguishing between all these cases really serves a purpose.