Binding, rebinding, mutable?

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.

6 Likes