Rust Interview Mastery · Chapter 03 03

Common Programming Concepts

Variables, mutability, shadowing, every scalar type, functions, and control flow as expressions.

11 questions 4 topics Foundation

Variables, Mutability & Shadowing

Why are Rust variables immutable by default?

Immutability-by-default is a deliberate design push: mutation is where bugs and data races breed. When a variable is immutable you can reason about it locally — nobody changes it under you. When you do need mutation, mut makes it a visible, greppable decision.

It also feeds the concurrency story: aliased immutable data is trivially shareable across threads. The compiler can lean on that.

What is the difference between let, let mut, and const?

- let x = 5 — immutable binding, type inferred

- let mut x = 5 — mutable binding, can reassign (same type)

- const MAX: u32 = 100 — compile-time constant

const is stricter: type annotation required, always immutable, must be a constant expression (no runtime calls — const X: u32 = compute() won't compile). Constants get inlined at use sites; naming convention is SCREAMING_SNAKE_CASE. And unlike let, you can't shadow a const with mut semantics — constants don't live on the stack.

Shadowing vs mutation — what's the actual difference?

Mutation keeps one binding and changes its value (x = x + 1, same type, needs mut). Shadowing declares a new binding that hides the old one (let x = x + 1), and it may change type.

let spaces = "   ";
let spaces = spaces.len();  // &str -> usize, still immutable

Shadowing is also scoped: inside a block the new binding wins; when the block ends, the outer binding re-emerges. Interviewers probe this because it trips up people coming from JS/Python.

Data Types

What are Rust's scalar types, and which are the defaults?

Four scalar families:

- Integers: i8..i128, u8..u128, plus isize/usize (pointer-sized — indexing, sizes). Default: i32.

- Floats: f32, f64. Default: f64 — modern CPUs do double precision at the same speed.

- Boolean: bool — true/false, one byte.

- Character: char — 4 bytes, a Unicode scalar value, not ASCII. '😻' is a valid char.

usize for indexing is worth naming: it keeps arrays/index math tied to the platform's pointer width.

What happens on integer overflow?

Depends on the build. In debug, overflow panics — the program crashes loudly so you notice. In release, it wraps around with two's complement (u8 255 + 1 → 0) — silent.

This is a classic correctness trap, so Rust gives explicit APIs when wrapping might be intended: wrapping_add, checked_add (returns Option), saturating_add (clamps), overflowing_add (returns value + flag). In an interview, "it wraps in release" is the wrong stopping point — name the explicit methods.

Tuple vs array — when do you use which?
let t: (i32, f64, &str) = (500, 6.4, "hi");  // tuple: heterogeneous
let a: [i32; 5] = [1, 2, 3, 4, 5];          // array: homogeneous

Tuples group different types, fixed arity, accessed by destructuring (let (x, y, z) = t) or index (t.0). They're the poor-man's record — returning multiple values from a function.

Arrays hold one type, fixed length known at compile time, allocated on the stack. Access a[0]; out-of-bounds panics at runtime. Growable sequences are Vec, not arrays.

Functions & Expressions

Statements vs expressions — why do interviewers keep asking this?

Statements perform an action and return nothing: let x = 5; is a statement (you can't write let y = (let x = 5)). Expressions evaluate to a value: 5 + 6, function calls, macro calls, if/match, and blocks { ... }.

The rule that bites people: a block's value is its last expression without a semicolon. Add ; and it becomes a statement — the block returns ().

let y = {
    let x = 3;
    x + 1        // no semicolon -> y == 4
};

This is why Rust function bodies can omit return — the tail expression is the return value.

Explain Rust function signatures: parameters, returns, and the implicit return rule.
fn five() -> i32 { 5 }              // implicit return
fn plus_one(x: i32) -> i32 { x + 1 }
fn plus_two(x: i32) -> i32 {
    return x + 2;                    // explicit return, early exit style
}

Parameter types are mandatory — no inference at function boundaries, because signatures are the API contract. The -> T declares the return type; omit it and you return () (unit).

Style: implicit tail return for the normal path, return for early exits (guards).

Control Flow

How is if different in Rust than in C or Python?

if is an expression, so it produces a value:

let number = if condition { 5 } else { 6 };   // ternary, but spelled out

Both arms must yield the same type — if c { 5 } else { "six" } won't compile. No parentheses around the condition. Conditions must be bool — if number {} where number is i32 is an error; Rust won't truthiness-coerce like Python or JS.

What are loop, while, and for — and when does each win?

- loop {} — unconditional, runs until break. Unique trick: break can return a value — let r = loop { break r * 2; };

- while cond {} — classic pre-check loop; often replaceable by for + iterators

- for item in collection/range {} — the everyday choice: for i in 0..10, for x in vec.iter()

Rust also supports loop labels for nested control: 'outer: loop { loop { break 'outer; } }. Prefer for when iterating — it can't overrun the end like a manual index loop can.

What's the performance/safety angle of for-in-range vs while with an index?

for i in 0..len { a[i] } is safe and the compiler can often elide bounds checks because the range is provably in-bounds. A manual while i < len { ...; i += 1 } gives the optimizer less to work with and gives you an off-by-one to mess up.

Even better is for x in slice.iter() — no indexing at all. This is chapter 13's iterator story previewed in chapter 3.