@m-n's answer set me on the right track. It's all about stack addresses! Here's a demonstration that solidified in my mind what's actually happening.
struct Point {
x: i64,
y: i64,
}
fn main() {
{
println!("== clobber binding");
let a = 1;
println!("val={} | addr={:p}", a, &a);
// This is completely new variable, with a different stack address
let a = 2;
println!("val={} | addr={:p}", a, &a);
}
{
println!("== reassign");
let mut b = 1;
println!("val={} | addr={:p}", b, &b);
// uses same stack address
b = 2;
println!("val={} | addr={:p}", b, &b);
}
{
println!("== Struct: clobber binding");
let p1 = Point{ x: 1, y: 2 };
println!(
"xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
p1.x, p1.y, &p1, &p1.x, &p1.y);
let p1 = Point{ x: 3, y: 4 };
println!(
"xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
p1.x, p1.y, &p1, &p1.x, &p1.y);
}
{
println!("== Struct: reassign");
let mut p1 = Point{ x: 1, y: 2 };
println!(
"xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
p1.x, p1.y, &p1, &p1.x, &p1.y);
// each of these use the same addresses; no new addresses
println!(" (entire struct)");
p1 = Point{ x: 3, y: 4 };
println!(
"xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
p1.x, p1.y, &p1, &p1.x, &p1.y);
println!(" (individual members)");
p1.x = 5; p1.y = 6;
println!(
"xval,yval=({}, {}) | pointaddr={:p}, xaddr={:p}, yaddr={:p}",
p1.x, p1.y, &p1, &p1.x, &p1.y);
}
}
Output (addresses are obviously slightly different per run):
== clobber binding
val=1 | addr=0x7fff6112863c
val=2 | addr=0x7fff6112858c
== reassign
val=1 | addr=0x7fff6112847c
val=2 | addr=0x7fff6112847c
== Struct: clobber binding
xval,yval=(1, 2) | pointaddr=0x7fff611282b8, xaddr=0x7fff611282b8, yaddr=0x7fff611282c0
xval,yval=(3, 4) | pointaddr=0x7fff61128178, xaddr=0x7fff61128178, yaddr=0x7fff61128180
== Struct: reassign
xval,yval=(1, 2) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
(entire struct)
xval,yval=(3, 4) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
(individual members)
xval,yval=(5, 6) | pointaddr=0x7fff61127fd8, xaddr=0x7fff61127fd8, yaddr=0x7fff61127fe0
The key points are these:
- Use
let
to "clobber" an existing binding (new stack address). This happens even if the variable was declared mut
, so be careful.
- Use
mut
to reuse the existing stack address, but don't use let
when reassigning.
This test reveals a couple of interesting things:
- If you reassign an entire mutable struct, it's equivalent to assigning each member individually.
- The address of the variable holding the struct is the same as the address of the first member. I guess this makes sense if you're coming from a C/C++ background.