Variables & Mutability
Variables in Rust: bindings, shadowing, mutability, constants, statics, type inference, and shadowing vs reassignment.
Rust — variables and bindings
EXAMPLE
// ===== let creates an immutable binding =====
fn main() {
let count = 0;
// count = count + 1; // compile error: cannot assign twice to immutable
// ===== mut for mutation =====
let mut total = 0;
total += 10;
total += 5;
println!("{}", total); // 15
// ===== Type inference + annotations =====
let x = 42; // i32 inferred
let y: u64 = 42; // explicit
let z = 1_000_000; // underscores allowed in numeric literals
let pi: f64 = 3.14;
let on = true;
let ch: char = 'Z'; // 4-byte Unicode scalar
let name = String::from("Alex");
// ===== Shadowing: a new binding with the same name =====
let s = "42";
let s: i32 = s.parse().expect("int"); // s now an i32; old binding shadowed
let s = s + 1; // shadow again
println!("{}", s);
// Shadowing is NOT mutation: type can change, the new binding is fresh.
// ===== const: compile-time constant =====
const MAX_USERS: usize = 100; // type required, must be const-evaluable
println!("max = {}", MAX_USERS);
// ===== static: program-lifetime memory =====
static GREETING: &str = "hello"; // immutable static; takes a fixed address
println!("{}", GREETING);
// static mut exists but requires `unsafe` and is almost always the wrong tool.
// ===== Scope =====
let outer = 1;
{
let inner = outer + 1;
println!("{}", inner);
} // inner dropped here
// println!("{}", inner); // error: inner not in scope
// ===== Destructuring =====
let (a, b) = (1, 2);
let [first, .., last] = [10, 20, 30, 40];
println!("{} {} {} {}", a, b, first, last);
// ===== Pattern bindings in match =====
let pair = (3, -7);
match pair {
(0, 0) => println!("origin"),
(x, 0) | (0, x) => println!("on axis at {}", x),
(x, y) if x.abs() == y.abs() => println!("diagonal"),
(x, y) => println!("point {},{}", x, y),
}
// ===== Strings: &str vs String =====
let borrowed: &str = "static slice"; // string slice into static memory
let owned: String = String::from("heap-allocated, growable");
let _push: String = owned + " + more"; // String supports + (consumes owned)
// ===== Move vs copy =====
let owned1 = String::from("a");
let owned2 = owned1; // owned1 moved into owned2
// println!("{}", owned1); // error: value used after move
let n1 = 42_i32;
let n2 = n1; // i32 is Copy; both valid
println!("{} {}", n1, n2);
// ===== Why immutability by default =====
// - Concurrency: shared, immutable data is data-race-free
// - Optimiser: more aggressive when bindings are stable
// - Code review: reassignment is rare and stands out when it appears
// ===== Patterns to internalise =====
// - Default to let, reach for let mut only where you genuinely mutate
// - Use shadowing to evolve a value's type through a function (parse -> validate)
// - const for things known at compile time; static rarely
// - Destructure aggressively in let and match; smaller bindings read better
// - Prefer &str function parameters; return String when you need ownership
}
// ===== Pitfalls =====
// - Forgetting mut and getting 'cannot borrow as mutable' -> add mut to the let
// - Shadowing inside an inner scope and being surprised when outer reverts
// - 'Variable' vs 'binding': move semantics mean the binding stops being valid
// - Using static mut: requires unsafe and is almost always wrong
// - const fn limits: not all expressions are const-evaluable yet
Why it matters
Immutable by default + shadowing + clear move semantics is what makes Rust feel rigorous. Reach for let, reach for mut only when you mean to mutate, and lean on shadowing when a value goes through transforming stages. The compiler stops bugs that other languages need tests to catch.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
let x = 5; // immutable by default let mut y = 10; // mutable const MAX: u32 = 100;Try it Yourself »
Exercise
Make a variable mutable.
let
count = 0;
Three letters.
Discussion
Loading…