Syntax and Variables
Variables, immutability and mut, scalar and compound types, control flow, and functions in Rust.
Variables are immutable by default
This is one of Rust's most distinctive design choices: variables declared with let cannot be reassigned unless you explicitly opt in with mut.
fn main() {
let x = 5;
println!("x is {x}");
x = 6; // compile error: cannot assign twice to immutable variable `x`
}
fn main() {
let mut x = 5;
println!("x is {x}");
x = 6; // fine — x was declared mutable
println!("x is now {x}");
}
Immutability by default isn't just a style preference — it means that when you read let x = ... anywhere in a Rust codebase, you know x never changes for the rest of its scope, unless the code explicitly says mut. That guarantee makes code far easier to reason about, especially in concurrent contexts.
Constants
const is stricter than an immutable let: the value must be a compile-time constant (no function calls that aren't const fn), the type annotation is mandatory, and it can be declared in any scope, including global:
const MAX_CONNECTIONS: u32 = 100;
fn main() {
println!("Limit: {MAX_CONNECTIONS}");
}
Shadowing
Rust lets you declare a new variable with the same name, "shadowing" the previous one — this is different from mutation, because it can even change the type:
fn main() {
let spaces = " "; // spaces: &str
let spaces = spaces.len(); // spaces: usize — a completely new binding
println!("{spaces}"); // 3
}
This is idiomatic Rust for transforming a value through a pipeline of steps without needing mut or inventing a new name at every step.
Scalar types
let i: i32 = -42; // signed 32-bit integer (i32 is the default integer type)
let u: u64 = 42; // unsigned 64-bit integer
let f: f64 = 3.14; // 64-bit float (the default float type)
let b: bool = true;
let c: char = 'R'; // a Unicode scalar value, 4 bytes — not just ASCII
Rust's integer types are named by size and signedness: i8/u8 through i128/u128, plus isize/usize (pointer-sized, used for indexing). Choosing the smallest type that fits your data is common in performance-sensitive code; i32 is the sensible default otherwise.
Compound types
// Tuple — fixed size, elements can have different types
let person: (&str, i32) = ("Ali", 22);
println!("{} is {}", person.0, person.1); // access by index
let (name, age) = person; // destructuring
println!("{name} is {age}");
// Array — fixed size, all elements the same type, stored on the stack
let nums: [i32; 3] = [1, 2, 3];
println!("{}", nums[1]); // 2
println!("{}", nums.len()); // 3
For a growable list, Rust's standard library provides Vec<T> (a heap-allocated, resizable vector) — arrays are for a fixed, known-at-compile-time size.
let mut scores: Vec<i32> = Vec::new();
scores.push(90);
scores.push(85);
println!("{:?}", scores); // [90, 85]
Control flow
if is an expression in Rust, not just a statement — it can produce a value:
let number = 7;
if number % 2 == 0 {
println!("even");
} else {
println!("odd");
}
let description = if number % 2 == 0 { "even" } else { "odd" };
println!("{number} is {description}");
Both branches of an if used as an expression must return the same type.
Loops
// loop — infinite, until an explicit break (can even return a value)
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // returns 20 from the loop
}
};
// while — runs while a condition holds
let mut n = 3;
while n != 0 {
println!("{n}!");
n -= 1;
}
// for — the idiomatic way to iterate a range or collection
for i in 0..5 { // 0..5 is exclusive of 5: 0,1,2,3,4
println!("{i}");
}
for item in [10, 20, 30] {
println!("{item}");
}
for over a range or an iterator is idiomatic Rust — it's both safer (no manual index bookkeeping) and, thanks to compiler optimizations, usually just as fast as a manually indexed loop.
Functions
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon — this is the returned expression, equivalent to `return a + b;`
}
fn main() {
let sum = add(2, 3);
println!("{sum}"); // 5
}
Parameter types and the return type must always be explicitly annotated on a function signature — Rust infers types for local variables, but never across function boundaries.
Common mistakes
- Adding a semicolon after the final expression of a function body when you meant to return its value —
a + b;becomes a statement that returns()(unit), not the sum. - Forgetting
mutand being confused by a "cannot assign twice to immutable variable" error — this is Rust working as intended, not a bug. - Confusing arrays (
[i32; 3], fixed size, stack-allocated) withVec<i32>(growable, heap-allocated) — reach forVecunless you specifically need a fixed-size, stack-allocated collection.
Interview questions
Q: Why are variables immutable by default in Rust?
It makes the common case — a value that's set once and read many times — safe and explicit by default, and forces you to opt in to mutability with mut only where it's genuinely needed. This reduces accidental mutation bugs and makes code easier to reason about, especially when shared across threads.
Q: What's the difference between shadowing and mutation?
Mutation (mut) changes the value stored in the same binding, and the type can never change. Shadowing creates an entirely new variable that happens to reuse the same name — the old value is inaccessible, and the new binding can even have a different type.