In Rust, an immutable variable can be
- initialized once
- then used immutably for as long as you want
- deinitialized (i.e. moved out of) once
Often times, initialization coincides with declaration, and deinitialization coincides with dropping at the end of the scope.
{
let a = String::from("Hello World"); // declaration and initialization
println!("{a}"); // use immutably
} // end of scope, value of `a` is dropped here
But you can initialized late or deinitialize early.
{
let a; // declaration
// let _ = &a; // trying to read `a` at this point would be a compilation error
println!("variable a about to be initialized");
a = String::from("Hello World"); // initialization
println!("{a}"); // use immutably
drop(a); // de-initialize early; could also move a to other function or variable
println!("a has been dropped already");
// let _ = &a; // trying to read `a` at this point would be a compilation error
} // end of scope, value of `a` was already moved-out-of so nothing happens here
Whether or not you interpret these two state changes (uninitialized -> initialized) / (initialized -> uninitialized) as a form of “mutation” of course could constitute a philosophical discussion of programming language design–anyways, this is how Rust’s design sees it: it’s not a mutation.
In my personal view, this makes sense. The main point of marking a variable as immutable is that at any place where the variable is used, you can be certain it still has the same value that it was initialized with.
Rust prevents access to a variable before it was initialized or after it has been deinitialized, hence this property that “you can be certain it still has the same value that it was initialized with” is always true, even if late initialization or early deinitialization is permitted.
The main strength of this “you can be certain it still has the same value that it was initialized with” property is that when reading code, you can easily just jump to the “definition” of an immutable variable to get its meaning, without needing to look at all the other places it's mentioned in between. To that end, early deinitialization poses the least concern in my view, and other features like shadowing, or late initialization, make it harder to find this “definition”. (In the case of shadowing, IDE support to “jump to definition” can still help avoid any issues; and late initialization is used not too commonly, anyways, because let
is allowed in the middle of blocks already.)