Variables, types, mutability
Immutable by default. let mut opts in. Shadowing lets you reuse a
name with a new type — encouraged, not avoided. Static types with strong inference
everywhere. Conversions are explicit: the compiler never silently truncates.
1 · The intuition
In Rust, variables are immutable by default. That's not a style choice — it's a
compile-time invariant the borrow checker leans on. To mutate, you write
let mut. The opposite of Go's "everything is mutable, except
const". Once you stop fighting it, it forces you to think about
where mutation lives.
let binds a name. let mut
binds a mutable name. const declares a compile-time constant.
There's also static for module-level data with a fixed address —
rare in day-to-day Rust.2 · Try it
fn main() {
// Immutable — the default
let name = "Rust";
// Mutable
let mut count = 0;
count += 1;
count += 1;
// Type annotation when you want it
let age: u8 = 30;
// Shadowing — reuse the name, possibly with a new type
let value = "5";
let value: i32 = value.parse().unwrap();
let value = value + 10;
println!("{} count={} age={} value={}", name, count, age, value);
}3 · The numeric types
| Signed | Unsigned | Use for |
|---|---|---|
i8 / i16 / i32 / i64 / i128 | u8 / u16 / u32 / u64 / u128 | Specific widths — protocols, bit-packing. |
isize | usize | Pointer-sized — array indices, lengths. |
f32 / f64 | Floats. f64 is the default. | |
bool | true / false. 1 byte. | |
char | A single Unicode code point, 4 bytes. | |
Default integer is i32. Default float is f64. Use
usize for anything index-shaped — Rust will complain when you mix
usize with smaller integers.
4 · Explicit conversions with as
fn main() {
let x: i32 = 42;
let y: u8 = x as u8; // explicit, may truncate
let z: f64 = x as f64;
println!("x={} y={} z={}", x, y, z);
// Won't compile:
// let bad: u8 = x; // mismatched types
}Unlike C, Rust never auto-converts. Even i32 to i64
needs an as. The verbosity is intentional — every potential loss
of information is visible in the source.
5 · Shadowing — the underrated feature
fn main() {
let spaces = " "; // a &str
let spaces = spaces.len(); // now a usize — same name, different type!
println!("{}", spaces); // 3
}In most languages re-binding with a new type would be a workaround. In Rust it's
the idiom — you avoid intermediate names like spaces_count and the
compiler still type-checks every line.
let mut. Shadowing creates a new
binding (different memory, different type allowed). let mut reassigns
the same binding (same type, same memory). Pick shadowing when the type changes;
pick mut when the value changes but the type stays.6 · Constants & statics
// const — inlined at compile time, type required, must be a const-expr
const MAX_USERS: u32 = 100_000;
const PI: f64 = 3.14159;
// static — lives at a fixed address for the program's lifetime
static GREETING: &str = "Hello";
// static mut — possible, but unsafe (needs unsafe blocks to touch)
static mut COUNTER: u32 = 0;Shows in numeric literals are visual grouping: 1_000_000 is
one million. The compiler ignores them.
7 · Common mistakes
- Trying to mutate without
mut.let x = 0; x = 1;won't compile. Addmut. - Forgetting the explicit cast. Mixing numeric types needs
asevery time. No implicit promotion. - Confusing
constandlet.constmust be a compile-time expression, type-annotated, written in SCREAMING_SNAKE.letis runtime binding. - Integer overflow. In debug builds, overflow panics. In release builds, it wraps. Use
checked_add/saturating_add/wrapping_addwhen you care. - Comparing different numeric types.
let a: i32 = 5; let b: usize = 5; a == b— doesn't compile. Convert one side.
8 · Exercises (~10 min)
- Fahrenheit-to-Celsius. Write a function that takes
f64and returnsf64. Use it in main. - The shadow trick. Read a string from
std::env::args(), shadow it to ani32viaparse, then shadow again withn * 2. Three lines, three types, one name. - Overflow on purpose. Set
let x: u8 = 255; let y = x + 1;. Run debug. Runcargo run --release. Note the difference. - The bool size. Use
std::mem::size_of::<bool>(). Report. Nowchar— guess before running.
9 · When it clicks
- You write
letby reflex;mutonly where you mutate. - You stop being surprised that
i32andi64are different types. - Shadowing for type-conversion feels normal, not weird.
- You can predict integer-overflow behaviour for the build mode you're in.