Variables and Immutability by Default
One of Rust's most distinctive features is that variables are immutable by default. This is not an accident or a limitation - it is a deliberate design decision that prevents an entire class of bugs.
fn main() {
let x = 5;
println!("x = {x}");
// This would NOT compile:
// x = 6; // ERROR: cannot assign twice to immutable variable
// To make it mutable, use mut:
let mut y = 5;
println!("y = {y}");
y = 6; // This works!
println!("y = {y}");
}Why immutability by default? When you know a value will not change, the compiler can:
- Reason about your code more easily (and so can you)
- Enable optimizations
- Make multi-threaded code safer (we will see this in Module 9)
Start with let (immutable). Only add mut when the compiler tells you the variable needs to change. This makes your code's intent explicit.
Constants
Constants are different from immutable variables. They are truly constant - computed at compile time, valid for the entire program lifetime, and can be used in any scope:
// Constants use SCREAMING_SNAKE_CASE
// Type annotation is required
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265358979;
// In a struct/impl context
const SPEED_OF_LIGHT: u64 = 299_792_458; // meters per second
fn main() {
println!("Max points: {MAX_POINTS}");
println!("Pi: {PI}");
// Note: underscores in numeric literals improve readability
let one_million = 1_000_000;
let hex = 0xFF_AA_BB;
let binary = 0b1111_0000;
println!("{one_million}, {hex}, {binary}");
}Constants vs immutable variables:
- Constants require explicit type annotation
- Constants can be global (outside functions)
- Constants are inlined at compile time - no memory address
- Constants can only be set to constant expressions, not runtime values
Shadowing
Rust allows you to shadow a variable by declaring a new one with the same name. This is different from mutation:
fn main() {
let x = 5;
println!("x = {x}"); // 5
let x = x + 1; // Shadow x with a new binding
println!("x = {x}"); // 6
{
let x = x * 2; // Shadow only in this block
println!("x = {x}"); // 12
}
println!("x = {x}"); // 6 (inner shadow gone)
// Key power: shadowing can change the type!
let spaces = " "; // &str type
let spaces = spaces.len(); // usize type
println!("spaces: {spaces}"); // 3
// With mut, you cannot change type:
let mut spaces2 = " ";
// spaces2 = spaces2.len(); // ERROR: mismatched types
}Shadowing is useful when you want to transform a value and give the result the same conceptual name, or when parsing input from one type to another.
Scope and Ownership Preview
Variables in Rust are scoped to their enclosing block. When they go out of scope, memory is automatically freed:
fn main() {
// x is not yet valid here
{
let x = 5; // x is valid from here
println!("x = {x}");
} // x goes out of scope here, memory freed
// x is not valid here - compiler error if you try to use it
// Nested scopes
let outer = "I'm in outer scope";
{
let inner = "I'm in inner scope";
println!("{}", outer); // outer is accessible here
println!("{}", inner);
}
println!("{}", outer); // Still valid
// println!("{}", inner); // ERROR: inner not in scope
}This automatic cleanup when a variable goes out of scope is called "drop" in Rust. It is the foundation of Rust's memory management system, which we will explore deeply in Module 2.
The let Statement: Full Power
fn main() {
// Basic binding
let a = 5;
// With type annotation
let b: i32 = 5;
// Destructuring (more on this in Module 4)
let (x, y, z) = (1, 2, 3);
println!("x={x}, y={y}, z={z}");
// Destructuring with underscore (ignore)
let (first, _, last) = (1, 2, 3);
println!("first={first}, last={last}");
// let in if expression
let number = 7;
let description = if number % 2 == 0 { "even" } else { "odd" };
println!("{number} is {description}");
// Ignoring unused variables with underscore prefix
let _unused_variable = 42; // No warning from compiler
}🎯 Practice Exercise
- Create a program that declares a temperature in Celsius as an immutable
let - Use shadowing to convert it to Fahrenheit (reassign with same name)
- Create a constant
ABSOLUTE_ZERO_C: f64 = -273.15 - Print whether your temperature is above absolute zero
- Demonstrate that trying to mutate an immutable variable produces a compiler error, then fix it
🎉 Key Takeaways
- Variables are immutable by default in Rust - use
mutto allow mutation - Constants (
const) are compile-time values, require type annotation, can be global - Shadowing lets you re-declare a variable with the same name, even changing its type
- Variables are scoped to their block; memory is freed when they go out of scope
- Underscore prefix (
_name) suppresses unused variable warnings