Mutability
Variables are immutable by default in Rust — opt in with mut and let the compiler enforce the rest.
In R, most objects are effectively immutable — when you modify one, R copies
it first. This is called copy-on-write semantics. Rust takes a stricter
approach, making variables immutable by default, and the compiler enforces this
at compile time. To make a variable mutable, you must declare it with the mut
keyword:
let mut x = 5;
x = 6; // works because x is mutable
Without mut, attempting to reassign a variable is a compile error. This helps
catch bugs early by making data changes explicit. Note that when updating a
mutable variable, you do not re-bind it with let — you simply assign the new
value directly.
Mutability is especially useful for accumulating values across a loop. In the previous section, we saw that variables defined inside a loop cannot be accessed outside it. The solution is to declare a mutable variable in the outer scope and update it inside the loop:
src/main.rs
fn main() {
let numbers = vec![1, 2, 3];
let mut doubled = 0;
for n in numbers {
doubled = n * 2;
println!("{} doubled is {}", n, doubled);
}
// `doubled` is accessible here because it was declared outside the loop
println!("Last doubled: {}", doubled);
}1 doubled is 2
2 doubled is 4
3 doubled is 6
Last doubled: 6
You can also use += to add and assign in one step — x += 10 is equivalent
to x = x + 10. This makes computing running totals concise:
src/main.rs
fn main() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let n = x.len() as f64;
let mut total = 0.0;
for xi in x {
total += xi;
}
println!("The mean is: {}", total / n);
}The mean is: 2.5
Like scalar variables, vectors must also be declared mut to modify them after
creation. You can create an empty mutable vector and let Rust infer the data
type based on how you go on to use it:
src/main.rs
fn main() {
let mut names = Vec::new();
names.push("Alice");
names.push("Bob");
println!("{:?}", names);
}["Alice", "Bob"]
Here, Vec::new() creates an empty vector, and .push() appends an element to
the end. Because the element appended is a character string, the Rust compiler
is able to infer the type of the names vector at compile time. With this
example, we have also introduced another method, namely .push(), which is
particularly useful, but there are, in fact, many ways to modify mutable vectors
in Rust. Let us mention just three here.
Clear – Instead of adding elements, you can remove all elements at once
with .clear():
src/main.rs
fn main() {
let mut nums = vec![1, 2, 3];
nums.clear();
println!("{:?}", nums);
}[]
Sort – Vectors can be sorted in place with .sort(). Not all types support
ordering, but numeric types and strings do:
src/main.rs
fn main() {
let mut x = vec![11, 3, 7, 10, 1];
println!("x before sorting: {x:?}");
x.sort();
println!("x after sorting: {x:?}");
}x before sorting: [11, 3, 7, 10, 1]
x after sorting: [1, 3, 7, 10, 11]
Extend – To combine two vectors, use .extend(), which appends the
contents of one vector onto another:
src/main.rs
fn main() {
let mut a = vec![1, 2];
let b = vec![3, 4];
a.extend(b);
println!("{:?}", a);
}[1, 2, 3, 4]
Note that the vector being extended must be mut and that the second vector is
moved into the first — it can no longer be used after the call. We will
elaborate on that last point more in the section on Rust’s memory model,
specifically the concept of ownership.