dash dash force Misadventures in software engineering

Variable shadowing and scoping in Rust

Having been raised in the Mad Max-esque craziness that is JavaScript’s scoping system, learning Rust has been a real breath of fresh air. However that doesn’t make it totally free of some sneaky behaviors. One in particular is the concept of variable shadowing, and how it can lead to some odd code execution when re-declaring variables. Let’s take a look.

Scoping

Rust follows pretty straight forward rules when it comes to scoping. Generally, if a variable is declared within a {} block, the curly brackets define the scope by which that variable can be accessed. If you try to access the variable outside of it, you’ll compilation errors. For example:

fn main() {
    let alpha = 117;
    {
        let beta = 2077;
    }
    println!("value of beta: {}", beta);
}

will result in

 --> src\main.rs:8:38
  |
8 |     println!("Value of beta: {}", beta);
  |                                      ^^^^ not found in this scope

as the curly brackets around the declaration of beta create a scope outside of which it cannot be accessed. This is true of function declaration, loop iterations, etc. Also of note is that Rust has some seriously clear error messages.

Shadowing

But what if we try to access alpha inside the scoped block?

fn main() {
    let alpha = 117;
    {
        let beta = 2077;
        println!("Value of alpha: {}", alpha);
    }
}
Value of alpha: 117

Makes pretty good sense right? The inner scoped declared by the curly brackets gets access to all variables declared in the outer scope.

But what if we redeclare alpha inside the inner scope and then access it again in the outer scope?

fn main() {
    let alpha = 117;
    {
        let beta = 2077;
        let alpha = 451;
        println!("Value of inner alpha: {}", alpha);
    }
    println!("Value of outer alpha: {}", alpha);
}
Value of inner alpha: 451
Value of outer alpha: 117

Well that’s interesting. Declaring alpha again within the inner scope didn’t cause any compilation warning or error and seems to have created two totally separate variables. Inspecting the pointer addresses shows that that is indeed what has happened, signified by their different addresses.

fn main() {
    let alpha = 117;
    {
        let beta = 2077;
        let alpha = 451;
        println!("Value of inner alpha: {}", alpha);
        println!("Address of inner alpha: {:p}", &alpha);
    }
    println!("Value of outer alpha: {}", alpha);
    println!("Address of outer alpha: {:p}", &alpha);
}
Value of inner alpha: 451
Address of inner alpha: 0x620b6ff3dc
Value of outer alpha: 117
Address of outer alpha: 0x620b6ff3d8

So what if we try to declare a variable twice within the same scope?

fn main() {
    let alpha = 117;
    println!("Value of alpha: {}", alpha);
    println!("Address of alpha: {:p}", &alpha);
    let alpha = 451;
    println!("Value of alpha after re-declaration: {}", alpha);
    println!("Address of re-declared alpha: {:p}", &alpha);
}
Value of alpha: 117
Address of alpha: 0x6d7251f88c
Value of alpha after re-declaration: 451
Address of re-declared alpha: 0x6d7251f92c

Once again no errors or warning on compilation, and a second variable is created also of the name alpha with a different pointer that overrides the first alpha.

Updating within an inner scope

Updating the value of alpha within the inner block does indeed persist that change to the outer scope without creating a new variable as seen here:

fn main() {
    let mut alpha = 117;
    println!("value of alpha after declaration: {}", alpha);
    {
        alpha = 451;
        println!("value of inner alpha: {}", alpha);
        println!("Address of inner alpha: {:p}", &alpha);
    }
    println!("Value of outer alpha: {}", alpha);
    println!("Address of outer alpha: {:p}", &alpha);
}
value of alpha after declaration: 117
value of inner alpha: 451
Address of inner alpha: 0x5f02eff4d4
Value of outer alpha: 451
Address of outer alpha: 0x5f02eff4d4

May your code be well oxidized.