Skip to content

← Ground Rules: Values, Types, Control Flow step 16 of 24

Medium Primitives

Loop labels and labeled blocks

Find a value in a grid of rows and return its [row, column], or [-1, -1] if it is not there. Scan row by row, left to right, and stop at the first match.

pub fn find_in_grid(grid: &[Vec<i32>], target: i32) -> Vec<i64>

Rows may have different lengths — that is legal and there is a test for it. An empty grid gives [-1, -1].

The starter does not compile: it uses a label nobody declared.

break leaves one loop. A label says which one.

Plain break leaves the innermost enclosing loop. In a nested search that is almost never what you want — you have found the answer, and you would like to leave both loops.

The C-shaped answer is a flag threaded through both levels:

let mut found = false;
for row in grid {
    for value in row {
        if *value == target { found = true; break; }
    }
    if found { break; }
}

That is four extra lines of bookkeeping whose only job is to carry one bit upward, and it is exactly the kind of hand-maintained state Rust otherwise helps you avoid. So the language lets you name a loop:

'outer: for row in grid {
    for value in row {
        if *value == target {
            break 'outer;      // leaves BOTH loops
        }
    }
}

A label is a lifetime-style name — an apostrophe followed by an identifier — written before the loop keyword. break 'name and continue 'name then target that loop specifically. Using a label that does not exist is E0426: use of undeclared label, which is what the starter produces. (Its cousin E0767, “use of unreachable label”, is what you get when the label exists but is not on an enclosing construct.)

Labeled blocks: the part most people never learn

Since Rust 1.65 you can label a plain block, and break out of it with a value:

let answer = 'search: {
    for (r, row) in grid.iter().enumerate() {
        for (c, value) in row.iter().enumerate() {
            if *value == target {
                break 'search vec![r as i64, c as i64];
            }
        }
    }
    vec![-1, -1]
};

Read it as a small, structured early return that only escapes as far as the block. The break 'search expr supplies the block’s value; if the loops finish without breaking, the block falls through to its own tail expression, which is the not-found answer. Both paths produce the same type, so the whole block is an expression you can bind — or, as here, make the entire function body.

This is worth knowing well. It gives you the readability of an early return without actually returning, which matters as soon as there is cleanup to do after the search, and it keeps the found and not-found answers visibly adjacent instead of separated by the whole loop.

never_loop

One default-on lint to know about here: never_loop fires when a loop body can only ever execute once — typically because every path through it ends in break or return. That usually means you wrote a loop where you wanted an if, and it is a genuine bug catcher.

Types at the boundary

enumerate() gives usize indices; the return type is Vec<i64> so that the sentinel -1 is expressible. as i64 from usize is a widening on the 64-bit graders and cannot lose anything here.