These steps introduce the ideas that make Rust different: immutability by default, ownership and borrowing, Option and Result instead of null and exceptions, and pattern matching.
1. Variables are immutable unless you say otherwise
let binds a value that cannot change; let mut allows reassignment. Making mutation explicit is what lets the compiler reason about your program.
fn main() {
let name = "Rust";
let mut count = 0;
count += 1;
println!("{name} {count}");
}
2. Ownership: one owner at a time
Assigning a heap value moves it. The old binding becomes unusable, which is how Rust guarantees there is no double free without a garbage collector.
fn main() {
let a = String::from("code");
let b = a.clone(); // clone, because a move would end a's life
println!("{a} {b}");
}
3. Borrow instead of moving
& lends a read-only reference and &mut lends an exclusive one. You may have many readers or one writer, never both — that rule eliminates data races.
fn length(s: &String) -> usize { s.len() }
fn main() {
let s = String::from("compiler");
println!("{} {}", length(&s), s);
}
4. Option and Result replace null and exceptions
A value that may be missing is Option<T>; an operation that may fail is Result<T, E>. The compiler forces you to handle both cases.
fn main() {
let nums = vec![1, 2, 3];
match nums.get(5) {
Some(v) => println!("found {v}"),
None => println!("out of range"),
}
}
5. Match exhaustively
match must cover every variant, so adding a new enum case turns every unhandled site into a compile error rather than a silent bug.
enum Status { Draft, Published, Archived }
fn label(s: Status) -> &'static str {
match s {
Status::Draft => "draft",
Status::Published => "published",
Status::Archived => "archived",
}
}
fn main() { println!("{}", label(Status::Published)); }