Variables#
Bindings in Rust are immutable by default. let x = 1 binds;
let mut x = 1 allows reassignment. Two other forms exist:
const for compile-time constants and static for
program-lifetime addresses. Ownership governs lifetime; the
compiler enforces that exactly one binding owns each value (or
multiple references to it, none of them mutable while any read
exists).
/types`.
let#
The standard binding form.
let x = 10; // immutable; cannot reassign
let mut y = 20; // mutable
y = 30; // ok
// x = 11; // error: cannot assign to immutable
let port: u16 = 8080; // explicit type
let port = 8080_u16; // type via literal suffix
Type is usually inferred. The operator annotates exported APIs and any binding whose inferred type would be too wide.
Shadowing#
let reuses a name; the new binding shadows the old one.
Useful when you want to change the type of a value
without inventing a new name.
let n = "42"; // n: &str
let n: i32 = n.parse().unwrap(); // shadows; now n: i32
let n = n * 2; // shadows again
Shadowing is not mutation; the old value is still around if something else holds a reference to it.
const and static#
const is a compile-time constant; inlined wherever it is
used, no address.
const MAX_RETRY: u32 = 5;
const PI: f64 = 3.14159;
static is a single value with a fixed address that lives
the whole program. static mut requires unsafe to read
or write.
static GREETING: &str = "hello";
static mut COUNTER: u32 = 0; // every read / write needs unsafe
Avoid static mut; static with an
AtomicU32 or a Mutex is the safe equivalent.
use std::sync::atomic::{AtomicU32, Ordering};
static HITS: AtomicU32 = AtomicU32::new(0);
HITS.fetch_add(1, Ordering::Relaxed);
Ownership#
Every value has exactly one owner at a time. When the owner goes out of scope, the value is dropped (destructor runs, memory freed). Moving the value transfers ownership.
let s = String::from("hello");
let t = s; // move; s is no longer valid
// println!("{s}"); // error: borrow of moved value
For types that implement Copy (primitives, fixed arrays of
Copy, tuples of Copy), assignment copies instead of
moving.
let n = 10;
let m = n; // copies; n still valid
println!("{n} {m}");
Borrowing#
&x borrows immutably; &mut x borrows mutably. The
borrow checker enforces.
Any number of immutable borrows OR exactly one mutable borrow, never both at once.
Borrows cannot outlive the owner.
let mut s = String::from("hi");
let r1 = &s; // immutable borrow
let r2 = &s; // also fine
println!("{r1} {r2}");
// let r3 = &mut s; // error: cannot borrow as mut while immutable refs exist
let r4 = &mut s; // ok: r1/r2 no longer used past here
r4.push_str(", world");
The compiler tracks the last use of each borrow; a borrow ends when nothing reads it later (non-lexical lifetimes).
let-else#
let ... else { ... } binds when the pattern matches;
otherwise runs a diverging block (return, continue,
panic!).
let Ok(port) = "8080".parse::<u16>() else {
eprintln!("bad port");
return;
};
// port: u16 in scope here
Use let-else to flatten “happy path” code that
would otherwise nest inside match or if let.
Destructuring#
let accepts patterns; tuples, structs, and enums all
destructure.
let (host, port) = ("localhost", 8080);
let Point { x, y } = p;
let User { name, .. } = user; // .. ignores remaining fields
Scope and Drop#
A binding’s scope ends at the closing } of its enclosing
block. When the owner goes out of scope, the value is dropped
(the destructor runs).
{
let f = File::open("config")?;
// file open here
} // f dropped; file closed
For deterministic cleanup beyond drop (close a stream, release
a lock), the value’s Drop impl runs at scope exit. Rust has
no GC; this is the RAII model in full.
Mutability#
Mutability is part of the binding, not the value. To
mutate through a reference, the reference itself must be
&mut.
let mut v = vec![1, 2, 3]; // owner is mutable
v.push(4); // method takes &mut self
let r: &mut Vec<i32> = &mut v; // mutable borrow
r.push(5);