What is the usage of variable shadowing

why rust has this?
what the usage?

Let's say you create a file, and then want to wrap the handle with a BufWriter. Shadowing would allow you to do it this way:

let fd = File::create("test").unwrap();
let fd = BufWriter::new(fd);

Instead of:

let fd_raw = File::create("test").unwrap();
let fd = BufWriter::new(fd_raw);

I find it really useful, it keeps my code cleaner.

4 Likes

It can be useful when you want to give ownership of a clone away in a loop without inventing a new name.

fn main() {
    let shared = Arc::new(vec![1, 2, 3, 4, 5]);
    
    for i in 0..5 {
        // Make a clone whose ownership is given to the new thread.
        let shared = shared.clone();
        thread::spawn(move || {
            println!("{}", shared[i]);
        });
    }
}

playground

3 Likes

what about shadowing in the same scope

Generally shadowing in the same scope can be used for:

  1. Changing a variable from mutable to immutable and vice-versa.
  2. Changing the type of a variable.

The latter is the situation in @naim's post.

1 Like

Shadowing also resulted in two, very hard to find bugs in my code. (I created a temporary variable with the same name as a method parameter.) I'd like the option to turn on a warning for it.

In @alice's example, I prefer writing

let shared_clone = shared.clone()

It makes it clear that I've got a copy, which can be important when that thing is not wrapped in an Arc.

1 Like

yay, i think so.
there should a option to warn shadowing.
is there any?

For me at least, by far the biggest win from shadowing is the ability to do a "copy, paste, tweak" workflow without "tweak" having to include changing every single variable name in what you copied and pasted. Especially when writing unit tests like this:

    #[test]
    fn some_test() {
        setup_frobnication(...);

        let foo = Foo::new("simple");
        let bar = frobnicate(foo);
        assert_eq!(bar, 1);

        let foo = Foo::new("less simple");
        let bar = frobnicate(foo);
        assert_eq!(bar, 2);

        let foo = Foo::new("intimidating");
        let bar = frobnicate(foo);
        assert_eq!(bar, 3);

        let foo = Foo::new("terrifying");
        let bar = frobnicate(foo);
        assert_eq!(bar, 4);

        validate_frobnication_logs(...);
    }

I was pretty dubious on shadowing myself until I wrote something like this in some C++ unit tests and noticed that about half of my "compiler says NO" cycles were being wasted just on fixing up all the variable names. Not to mention the times a wrong name in tests like this compiled, seemed to pass, then later continued passing when I expected it to break and wasted a bunch of debugging time.

At least in my personal experience, I cannot recall a single time the C++ compiler complained about me shadowing a variable that it would've prevented a bug.

3 Likes

Clippy has the shadow_* lints to catch some shadowing.

4 Likes

not only in loops i think the combination of thread::spawn and shadowing already a neat combination.

fn main() {
    let shared = Arc::new(vec![1, 2, 3, 4, 5]);

    thread::spawn({
        let shared = shared.clone();
        move || {
            println!("{:?}", shared);
        }
    });

    thread::sleep(Duration::from_millis(50));
}
1 Like

With the following code, does it makes any benefit to not allow shadowing? I attached type annotations to every variables for clearness but in real code I'd omit most of them and let type inference fill it.

let config: &Path = &params.config;
let config: File = File::open(config)?;
let config: String = config.read_to_string()?;
let config: &str = config.split("#").nth(2).ok_or(...)?;
let config: Config = serde_json::from_str(config)?;
let config: Arc<Config> = Arc::new(config);
1 Like

I have done what you show, @Hyeonu, and I've often come to regret it when I decide I need one of those variables later in the code, most commonly for a trace record or an error message. I prefer more descriptive names, e.g.,

let path: &Path = &params.config;
let file: File = fs::open(path)?;
let file_contents: String = file.read_to_string()?;
let serialized_config: &str = file_contents.split("#").nth(2).ok_or(...)?;
let config: Config = serde_json::from_str(serialized_config)?;
let config_arc: Arc<Config> = Arc::new(config);

Picking decent names takes some mental effort, but I think the time is well spent.

1 Like

"To each their own."

3 Likes

"To each their own."

is something I should have included in my reply.

Well, yes and no.
If it's a personal project (which implies that you're the lead or only author) that's all fine and dandy.

But in projects (commercial or not) where multiple people become involved, a uniform style* is very important. So important is it that I have seen foreign styles just straight up eat 100% of my productivity, which is an undesirable outcome I never want to repeat. Therefore if the code is a mess in my mind, step 0 for me is: restyle or even rewrite such code.

Granted this is an issue I've never had in Rust, perhaps due to the existence of rustfmt**.

*which can include a list of verboten language features, as is often the case in the C++ world due to the sheer incidental complexity of the language

**I'm not all that sure how often rustfmt is really used in crates in practice, but when I read the code it is pretty much always very readable even if I would have written it differently. That's been a very different experience from a lot of (mostly imperative) languages I've used eg Java, C/C++ and pre-clojure lisps including Common Lisp.

Another use of variable shadowing which I will only occasionally use is to just shadow a variable with let foo = (); because I want it to go out of scope but for whatever reason it's inconvenient to actually move it into its own block...

Such as when I want to show that a function won't call itself recursively at some point.

pub fn main() {
    // main is not recursive
    #[allow(unused_variables)]
    let main = ();
    #[deny(unused_variables)]
}
2 Likes

First thing, when shadowing means you usually don't need the variable later since the ownership is moved. So you cannot do:

let config = parse(config);
let used_config = eat(config);
dbg!(config); // this was already moved

As for tracing the error later, one could do.

let config: &Path = &params.config;
let config: File = File::open(dbg!(config))?;
let config: String = config.read_to_string()?;
let config: &str = config.split("#").nth(2).ok_or(...)?;
let config: Config = dbg!(serde_json::from_str(config)?);
let config: Arc<Config> = Arc::new(config);

Just throw dbg!() to check stuff, I find that very useful without changing the structure of your code.

That explains our different approaches, @pickfire. I generate my trace records in a few places in my code where I collect a bunch of values and write out some JSON. Each of my trace records has a unique format identifier that I use in my trace analyzer. (I rolled my own before I learned about log! and dbg!) An example is

let trace_params = &TraceHeaderParams { module: file!(), line_no: line!(), function: "foo", format: "process_msg" };
let trace = json!({ "id": &self.my_id, "msg_no": msg_no, "msg": msg.value() });
let _ = add_to_trace(TraceType::Trace, trace_params, &trace, _f);

I see one place in your example where you shadow without moving ownership. That was exactly the bug that took me two days to find.

All that being said, the choice to shadow or not is a matter of taste and context.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.