As for whether this is good or bad, you can find a lot of past discussion of this if you search for "shadowing" in the various Rust forums. I think there are some reasonable arguments in both directions. It's definitely suprising to newcomers, but I haven't found it produces any real surprises once you're used to Rust semantics. It also allows some useful idioms.
Some notes that I hope might clarify things…
First, shadowing is a bit different from arbitrary mutation, if you look beyond simple types like integers. For example, if x
is an immutable binding to a struct:
let x = MyStruct::new(foo);
// ...
…then you can create a new x
bound to a totally new struct:
let x = MyStruct::new(bar); // okay!
…but you can’t mutate x
’s fields or borrow a mutable reference to it, unless you explicitly move it to a mutable binding:
x.name = "puppydog"; // ERROR
x.mutate(); // ERROR
let y = &mut x; // ERROR
Second, most other C-family languages also allow shadowing, but many restrict it to inner scopes shadowing outer scopes. For example, this is legal C or C++:
const int x = 0;
{
const int x = 1;
}
You can think of let
in Rust as always introducing a new scope. (This what let
does in OCaml, an important influence on Rust's syntax and semantics.) So this code:
let x = foo();
let y = bar();
// ...
is sugar for this:
let x = foo();
{
let y = bar();
// ...
}
This has several consequences, some of them subtle. One is that the lifetimes of these two variables are different; y
is destroyed before x
. (This matters in cases where you want to do something like borrow a reference to y
and store it in x
.) And in the case of shadowing, it means that this code:
{
let mut x = File::open("foo.txt");
let mut x = File::open("bar.txt");
}
is not equivalent to this code:
{
let mut x = File::open("foo.txt");
x = File::open("bar.txt");
}
If you run them under strace
you'll find they make a different sequences of system calls. The first one is equivalent to:
{
let mut x = File::open("foo.txt");
{
let mut x = File::open("bar.txt");
// both "foo.txt" and "bar.txt" are open here
} // "bar.txt" is closed here
} // "foo.txt" is closed here
while the second results in this sequence of actions:
{
let mut x = File::open("foo.txt");
x = File::open("bar.txt"); // "foo.txt" is closed here
// only "bar.txt" is open here
} // "bar.txt" is closed here
This difference is observable for any type with a destructor: Vec, Ref, MutexGuard, etc. It can also make a difference if there are live borrows of the shadowed value. With shadowing, the original value is still alive; it's just shadowed. Without shadowing, the original value is dropped when the new one is assigned.