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);

References#