In Chapter 3 of the Rust Book, Variables and Mutability, we go through a couple iterations on this theme in order to demonstrate the default, immutable behavior of variables in Rust:
fn main() {
let x = 5;
println!("The value of x is {}", x);
x = 6;
println!("The value of x is {}", x);
}
Which outputs:
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
3 | println!("The value of x is {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
However, because of Rust's take on shadowing variables, we can simply do this to change the value of the nonetheless "immutable" x
:
fn main() {
let x = 5;
println!("The value of x is {}", x);
let x = 6;
println!("The value of x is {}", x);
}
Which outputs (skipping the details):
The value of x is 5
The value of x is 6
Funnily enough, this code also produces the above pair of lines as output, despite the fact that we don't call let
but instead mut
the first time x
is bound to 5
:
fn main() {
let mut x = 5;
println!("The value of x is {}", x);
x = 6;
println!("The value of x is {}", x);
}
This ambiguity in how variables are (not really) protected from reassignment seems contrary to the stated goal of protecting the values bound to immutable - by Rust default - variables. From the same chapter (which also contains the section Shadowing):
It’s important that we get compile-time errors when we attempt to change a value that we previously designated as immutable because this very situation can lead to bugs. If one part of our code operates on the assumption that a value will never change and another part of our code changes that value, it’s possible that the first part of the code won’t do what it was designed to do. The cause of this kind of bug can be difficult to track down after the fact, especially when the second piece of code changes the value only sometimes.
In Rust, the compiler guarantees that when you state that a value won’t change, it really won’t change. That means that when you’re reading and writing code, you don’t have to keep track of how and where a value might change. Your code is thus easier to reason through.
If I can cause this important feature of my immutable x
to be side-stepped with an innocent enough call to let
, why do I need mut
? Is there some way to really, seriously-you-guys make x
immutable, such that no let x
can reassign its value?
Shadowing allows you to re-declare a variable in the same scope, using the same name. The re-declared variable differs from the original by having a different type. This is especially useful upon casting data from one type into another.
Shadowing is different from marking a variable as mut , because we'll get a compile-time error if we accidentally try to reassign to this variable without using the let keyword.
When a variable does need to change its value during run-time, the mut keyword can be used to indicate that the variable is mutable: 7.
It does, but Rust allows us to shadow the previous value of guess with a new one. Won't this feature just introduce problems like: hard to follow code (easier to create bugs) accessing variables when one intended to access a different variable (creates bugs)
I believe the confusion is because you're conflating names with storage.
fn main() {
let x = 5; // x_0
println!("The value of x is {}", x);
let x = 6; // x_1
println!("The value of x is {}", x);
}
In this example, there is one name (x
), and two storage locations (x_0
and x_1
). The second let
is simply re-binding the name x
to refer to storage location x_1
. The x_0
storage location is entirely unaffected.
fn main() {
let mut x = 5; // x_0
println!("The value of x is {}", x);
x = 6;
println!("The value of x is {}", x);
}
In this example, there is one name (x
), and one storage location (x_0
). The x = 6
assignment is directly changing the bits of storage location x_0
.
You might argue that these do the same thing. If so, you would be wrong:
fn main() {
let x = 5; // x_0
let y = &x; // y_0
println!("The value of y is {}", y);
let x = 6; // x_1
println!("The value of y is {}", y);
}
This outputs:
The value of y is 5
The value of y is 5
This is because changing which storage location x
refers to has absolutely no effect on the storage location x_0
, which is what y_0
contains a pointer to. However,
fn main() {
let mut x = 5; // x_0
let y = &x; // y_0
println!("The value of y is {}", y);
x = 6;
println!("The value of y is {}", y);
}
This fails to compile because you cannot mutate x_0
while it is borrowed.
Rust cares about protecting against unwanted mutation effects as observed through references. This doesn't conflict with allowing shadowing, because you're not changing values when you shadow, you're just changing what a particular name means in a way that cannot be observed anywhere else. Shadowing is a strictly local change.
So yes, you absolutely can keep the value of x
from being changed. What you can't do is keep what the name x
refers to from being changed. At most, you can use something like clippy
to deny shadowing as a lint.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With