Beginner question on best practice for loops

I'm just starting to learn Rust. I'm playing around with the guessing game example from the Book, using VSCode. For the loop that allows the player to keep guessing, I thought it made sense to define the variable store the guess outside the loop, something like:

let mut guess = 0; //u32
loop {
    guess = get_guess(); //fn to get a guess from stdin
    
    // check the guess, etc
}

This compiles, but VSCode flags the first line with a warning that guess gets overwritten before it gets read, and that makes sense. My thinking, though, is that it's more efficient to create guess outside the loop, rather than using let mut guess = get_guess() inside the loop. The former allocates the memory once, and it gets updated each loop, while the latter recreates guess each loop. Is that right? The Book uses the latter, although they define guess as a String. What's the best practice here? Thanks!

You can simply remove the unused assignment:

let mut guess; //u32
loop {
    guess = get_guess(); //fn to get a guess from stdin
    
    // check the guess, etc
}

As long as the compiler can see that the variable is guaranteed to be initialized before it is used, it allows you to defer the assignment for later. In contrast, this is not accepted:

    let mut guess; //u32
    loop {
        if true {
            guess = get_guess(); //fn to get a guess from stdin
        }
        // check the guess, etc
        println!("{guess}");
    }
  --> src/main.rs:12:20
   |
6  |     let mut guess; //u32
   |         --------- binding declared here but left uninitialized
7  |     loop {
8  |         if true {
   |            ---- if this `if` condition is `false`, `guess` is not initialized
9  |             guess = get_guess(); //fn to get a guess from stdin
10 |         }
   |          - an `else` arm might be missing here, initializing `guess`
11 |         // check the guess, etc
12 |         println!("{guess}");
   |                    ^^^^^ `guess` used here but it is possibly-uninitialized

Even though this "obviously" works, the compiler doesn't accept it because the assignment is behind a conditional. If we assign to it in an else branch, it's okay again:

    let mut guess; //u32
    loop {
        if true {
            guess = get_guess(); //fn to get a guess from stdin
        } else {
            guess = 0;
        }
        // check the guess, etc
        println!("{guess}");
    }
1 Like

A small difference such as this is something that could matter in a scripting language such as Python but won't in a language such as Rust that uses an optimizing compiler. Whichever way you write the code, a part of the compiler called the optimizer should rewrite the code to the other way if that would make it more efficient.

For something as trivial as u32, there's no difference, so you should declare it at the smallest scope possible.

The only time it's worth declaring it at a broader scope is if you want to reuse heap allocations, like re-using a String's buffer across iterations.

1 Like

There is no allocation on the heap in either case but only on the stack (see the stack and the heap). So there is memory "allocated" (edit: actually pushing values onto the stack isn't called "allocation" according to the explanation behind that link), but this is a very efficient operation. The compiler can also decide when to do it, so even if you write it inside the loop it may happen outside the loop.

If you only use guess inside the loop, I would declare it inside the loop. If you break the loop and want to access guess from outside the loop, you must declare it outside the loop.


However, in a different scenario, when there is a real heap allocation, it might make sense to do such an allocation outside a loop, for example:

fn bad() {
    for i in 0..10 {
        let v = vec![i, i+1]; // heap allocation for each iteration of the loop
        println!("{v:?}");
    }
}

Here vec![i, i+1] will actually allocate memory on the heap. I'm not sure if the Rust compiler is able (or allowed to) optimize this by allocating only once as the Vec is dropped at the end of each iteration. But in such a case, you might want to consider:

fn good() {
    let mut v = Vec::with_capacity(2);
    for i in 0..10 {
        v.clear(); // here we re-use the existing `Vec`
        v.push(i);
        v.push(i+1);
        println!("{v:?}");
    }
}

(Playground)

1 Like