The main intention for mutability markers on local variables in Rust, i.e. let mut x = …
vs let x = …
, but also for function arguments like x
vs mut x
as in your examples, is to help make code generally more predictable.
As such, even though the variable is considered “immutable”, actually two kinds of kind-of-mutating operations are allowed for an immutable variable: First initialization, and final moving-out. The rationale for this is that before first initialization and after finally moving-out of a variable, the variable is not accessible anyways (it may be in scope, but you’d get a compilation error if reading from the variable before it’s first initialized or after it’s been moved out of), so throughout the whole period where it is accessible, it will not change in value, which is sufficient to get the desired predictability you’d expect from immutable variables: The value is always the same it was when it was defined/initialized.
As some simple demonstration code, you can do
let x: String; // declaration
x = "Foo".to_string(); // initialization
drop(x); // moving out
all to an immutable variable x
. Declaration and initialization is often combined, and moving out of the value is sometimes skipped, so it’s just implicitly dropped, and even in cases where it does happen, it commonly happens to the whole value at once. But moving out individual fields, i.e. de-initializing the value step-by-step so to speak, is just as fine, and the rationale is the same.
struct Foo {
x: String,
y: String,
}
let v: Foo; // declaration
v = Foo {
x: "Foox".to_string(),
y: "Fooy".to_string(),
}; // initialization
drop(v.x); // moving out 1
drop(v.y); // moving out 2
(Initializing in multiple steps is not easily possible as far as I’m aware.)
There’s another interpretation of “writing” / “mutation” in Rust, slightly but significantly different from what mut
variables allow you, which is access via &mut …
references. To categorize the action of moving out of a field in the spectrum of accesses that references provide (between &T
and &mut T
), actually, (both first initializing a value and) moving out of a value are operations that are not possible via by-reference access at all. It’s a purely owned-access kind of operation to leave a value in an (true) moved-out-of state.
The workaround when working via references comes in the form that mutable references while not allowing moving out a value, will still allow replacing it, and as such, particularly for types like Option<T>
you can achieve what amounts to essentially moving out a value via functions like mem::take
or mem::replace
, or Option
’s take
method. In this case however, the value is still valid, initialized, and accessible, so this kind of operation is considered a true mutation, and requires actual mutable access and a mutable variable.
Another exception from normal “mutable vs. immutable variable” rules come from interior mutability types like RefCell
and co. I just wanted to mention that, as the rest of this response does brush over this exception, Rust does not help you make code more predictable in the same way it normally does, when these types are involved, so an immutable-marked variable with a type like RefCell<T>
can have its (inner) value actually changed.