3. Variables, mutability, shadowing, and scalar types
3.1 Immutable by default
fn main() {
let x = 5; // immutable binding
// x = 6; // error[E0384]: cannot assign twice to immutable variable
let mut y = 5; // `mut` is opt-in, and greppable
y += 1;
let x = x * 2; // SHADOWING: a brand-new variable that hides the old `x`
let spaces = " ";
let spaces = spaces.len(); // shadowing can even change the type: &str -> usize
println!("{x} {y} {spaces}"); // 10 6 3
}
mut belongs to the binding, not to the type. let mut v = Vec::new()
means "this variable may be reassigned or mutated through". Later you will
see the same distinction between &T and &mut T.
3.2 Scalar types, and the sizes that are actually guaranteed
| Rust | Size | Go | C |
|---|---|---|---|
i8 i16 i32 i64 i128 |
exact | int8 … int64 |
int8_t … (<stdint.h>) |
u8 u16 u32 u64 u128 |
exact | uint8 … uint64 |
uint8_t … |
isize usize |
pointer width | int uint |
ptrdiff_t size_t |
f32 f64 |
IEEE 754 | float32 float64 |
float double |
bool |
1 byte, only 0 or 1 | bool |
_Bool |
char |
4 bytes, a Unicode scalar value | rune |
— (char is a byte) |
Unlike C §3.1,
every integer type has a fixed size. Indexes and lengths are always
usize, so you will write as usize often at first.
3.3 Integer overflow: defined, but different in debug and release
In C, signed overflow is UB. In Go, it silently wraps. Rust's behavior is defined in every case, and you choose it:
fn main() {
let a: u8 = 250;
// In a DEBUG build, `a + 10` panics: "attempt to add with overflow".
// In a RELEASE build it wraps to 4 (two's complement), unless you set
// `overflow-checks = true` in [profile.release].
// When overflow matters, say what you want explicitly:
println!("{:?}", a.checked_add(10)); // None
println!("{}", a.wrapping_add(10)); // 4
println!("{}", a.saturating_add(10)); // 255
println!("{:?}", a.overflowing_add(10)); // (4, true)
// Conversions are explicit. `as` truncates silently; `try_from` checks.
let big: i64 = 300;
let truncated = big as u8; // 44, silently
let checked = u8::try_from(big); // Err(TryFromIntError(()))
println!("{truncated} {checked:?}");
}
Real-world example. Parsing a length field from a network packet
(Ch 45): let len = u16::from_be_bytes([b0, b1]) as usize; is safe because
u16 → usize can never lose data. Going the other way (usize → u16 when
writing a packet) must use u16::try_from(len)?. With as, a 70,000-byte
payload would quietly be encoded as a 4,464-byte length, which is the
classic framing bug behind many C parser CVEs.
Production setting: many security-sensitive projects set
overflow-checks = true in [profile.release]. The cost is usually under
a few percent. Profile your own workload before deciding.
3.4 Tuples, arrays, and type inference
fn main() {
let pair: (i32, &str) = (200, "OK");
let (code, reason) = pair; // destructuring
println!("{code} {reason} {}", pair.0);
let a = [0u8; 4]; // [u8; 4], four zero bytes, on the stack
let b = [1, 2, 3]; // inferred [i32; 3]
println!("{} {:?} {}", a.len(), b, b[2]);
// b[3] would not compile here: the index is a constant, and the compiler knows the length.
// For a runtime index, an out-of-bounds access PANICS. It never reads past the end.
let i = std::env::args().count() + 5;
println!("{:?}", b.get(i)); // None: the non-panicking accessor
}
Bounds checks are the reason Rust programs cannot have the C guide's §29 stack overflow. They usually cost little, because LLVM removes them when it can prove the index is in range. Iterators (Ch 21) avoid them entirely.