Rust Basics
Ownership Β· Borrowing Β· Lifetimes Β· Enums Β· Structs Β· Pattern Matching Β· The Borrow Checker
Sheet 1 of 2
Rust 2021
Beginner
Printable
Variables & Mutability
let, mut & const
// Immutable by default let x = 5; // x = 6; β compile error! // Mutable β explicit opt-in let mut y = 5; y = 6; // β allowed // Constant β type required, no mut const MAX_POINTS: u32 = 100_000; // Shadowing β reuse name, new value let x = 5; let x = x + 1; // x = 6 let x = "now a string"; // type changes ok!
Scalar & Compound Types
// Integers let a: i32 = -42; // signed 32-bit let b: u64 = 100; // unsigned 64-bit let c: usize = 10; // pointer-sized // Float, bool, char let f: f64 = 3.14; let ok: bool = true; let ch: char = 'π¦'; // full Unicode // Tuple let tup: (i32, f64, bool) = (1, 2.0, true); let (x, y, z) = tup; // destructure tup.0; // index access // Array (fixed size) let arr: [i32; 3] = [1, 2, 3]; let zeros = [0; 5]; // [0,0,0,0,0]
Functions & Expressions
// Functions β snake_case fn add(a: i32, b: i32) -> i32 { a + b // no semicolon = return value } // Explicit return fn early(x: i32) -> i32 { if x < 0 { return 0; } x * 2 } // If is an expression let n = if x > 0 { 1 } else { -1 }; // Blocks are expressions too let val = { let a = 3; a * a // 9 β no semicolon };
Key mindset: In Rust, everything is immutable by default. You must explicitly opt into mutability with
mut. This forces you to think clearly about what changes β and the compiler enforces it.Ownership
The 3 Ownership Rules
// Rule 1: Each value has one owner let s1 = String::from("hello"); // Rule 2: Only one owner at a time let s2 = s1; // s1 is MOVED to s2 // println!("{}", s1); β error! s1 invalid // Rule 3: Owner drops β value freed { let s = String::from("hi"); } // s is dropped here β memory freed
Clone vs Copy
// Clone β deep copy (heap types) let s1 = String::from("hello"); let s2 = s1.clone(); // s1 still valid // Copy β stack types auto-copy // i32, f64, bool, char, (i32, i32) let x = 5; let y = x; // x still valid β copied println!("{} {}", x, y); // 5 5 β
Ownership & Functions
fn takes_ownership(s: String) { println!("{}", s); } // s dropped here let s1 = String::from("hi"); takes_ownership(s1); // s1 no longer valid here! // Return to give ownership back fn gives_back(s: String) -> String { s // ownership moved to caller }
Why ownership? Rust guarantees memory safety at compile time β no garbage collector, no runtime overhead, no dangling pointers, no double-free errors.
Borrowing & References
Immutable References (&)
fn length(s: &String) -> usize { s.len() // borrows, doesn't own } let s1 = String::from("hello"); let len = length(&s1); // pass reference println!("{} len={}", s1, len); // s1 still valid! β // Many immutable refs at once β ok let r1 = &s1; let r2 = &s1; // β allowed
Mutable References (&mut)
fn append(s: &mut String) { s.push_str(", world"); } let mut s = String::from("hello"); append(&mut s); println!("{}", s); // hello, world // RULE: only ONE &mut at a time let r1 = &mut s; // let r2 = &mut s; β compile error! // Can't mix & and &mut let r1 = &s; // let r2 = &mut s; β error!
Borrow checker rules: (1) Any number of
&T references, OR (2) exactly one &mut T β never both at the same time. This prevents data races at compile time.Lifetimes
Why Lifetimes Exist
// Dangling reference β Rust prevents: let r; { let x = 5; r = &x; // β compile error! } // x dropped here // r would be dangling β blocked β // Lifetime annotation syntax // 'a β a generic lifetime parameter // Tells compiler: "reference lives at // least as long as lifetime 'a"
Lifetime in Functions
// Return ref must live as long as inputs fn longest<'a>( x: &'a str, y: &'a str ) -> &'a str { if x.len() > y.len() { x } else { y } } let s1 = String::from("long string"); let result; { let s2 = String::from("xy"); result = longest(&s1, &s2); println!("{}", result); // β }
Lifetime Elision & 'static
// Elision β compiler infers common cases // These two are equivalent: fn first_word(s: &str) -> &str { &s[..1] } fn first_word<'a>(s: &'a str) -> &'a str { &s[..1] } // 'static β lives entire program let s: &static str = "I'm forever"; // Lifetime in structs struct Excerpt<'a> { part: &'a str, }
Lifetimes don't change how long references live β they only describe the relationship between lifetimes so the compiler can verify borrows are safe. Most of the time, elision rules mean you don't write them explicitly.
Structs
Define & Instantiate
struct User { username: String, email: String, active: bool, sign_ins: u64, } let mut user1 = User { username: String::from("alice"), email: String::from("a@b.com"), active: true, sign_ins: 1, }; user1.email = String::from("new@b.com");
Methods & Struct Update
impl User { // &self β immutable method fn greet(&self) -> String { format!("Hi {}!", self.username) } // &mut self β mutable method fn login(&mut self) { self.sign_ins += 1; } // Associated function (no self) fn new(name: &str) -> User { User { username: name.to_string(), email: String::new(), active: true, sign_ins: 0 } } } // Struct update syntax let user2 = User { email: String::from("x@y.com"), ..user1 // rest from user1 };
Enums
Basic Enum & Data Variants
// Simple enum enum Direction { North, South, East, West, } let d = Direction::North; // Variants can hold data enum Message { Quit, // no data Move { x: i32, y: i32 }, // struct-like Write(String), // tuple-like Color(u8, u8, u8), // multiple values } let m = Message::Write( String::from("hello"));
Option<T> β the null-free null
// Option replaces null in Rust enum Option<T> { Some(T), None, } let some_num: Option<i32> = Some(5); let no_num: Option<i32> = None; // Unwrap safely some_num.unwrap_or(0) // 5 no_num.unwrap_or(0) // 0 some_num.is_some() // true no_num.is_none() // true // Map over the value some_num.map(|n| n * 2) // Some(10)
Enum Methods with impl
impl Message { fn call(&self) { match self { Message::Quit => println!("Quit"), Message::Move{x,y} => println!("Move {x},{y}"), Message::Write(s) => println!("Write: {s}"), Message::Color(r,g,b) => println!("#{r}{g}{b}"), } } }
No null in Rust. The type system forces you to handle the
None case explicitly. This eliminates an entire class of null pointer bugs that plague other languages.Pattern Matching
match β exhaustive
let n = 3; match n { 1 => println!("one"), 2 | 3 => println!("two or three"), 4..=10 => println!("4 to 10"), _ => println!("other"), } // match is an expression let msg = match n { 1 => "one", 2 => "two", _ => "other", };
if let & while let
let config: Option<u8> = Some(7); // if let β single pattern match if let Some(val) = config { println!("config is {val}"); } // while let β loop while pattern matches let mut stack = vec![1,2,3]; while let Some(top) = stack.pop() { println!("{top}"); // 3, 2, 1 } // Destructure in match let point = (3, -5); match point { (0, 0) => println!("origin"), (x, 0) => println!("x-axis at {x}"), (0, y) => println!("y-axis at {y}"), (x, y) => println!("({x},{y})"), }
match is exhaustive. The compiler forces you to handle every possible variant β no forgotten cases. Use
_ as a catch-all when you don't need to handle the rest.Vec<T> & String
Vec β growable array
// Create let mut v: Vec<i32> = Vec::new(); let mut v = vec![1, 2, 3]; // Push & pop v.push(4); // [1,2,3,4] v.pop(); // Some(4) // Access v[0] // 1 β panics if OOB v.get(0) // Some(&1) β safe v.len() // 3 // Iterate for i in &v { println!("{i}"); } for i in &mut v { *i *= 2; }
String vs &str
// &str β string slice (immutable, borrowed) let s: &str = "hello"; // stored in binary // String β heap-allocated, growable let mut s = String::from("hello"); s.push_str(", world"); s.push('!'); s.len() // bytes, not chars s.is_empty() // bool s.contains("world") // bool s.to_uppercase() // new String // &String auto-coerces to &str fn greet(s: &str) {} greet(&s); // β deref coercion
Control Flow & Loops
if / else if / else
let n = 7; if n < 5 { println!("small"); } else if n == 7 { println!("lucky!"); } else { println!("other"); } // if as expression let label = if n % 2 == 0 { "even" } else { "odd" }; // "odd"
loop Β· while Β· for
// loop β infinite, return value with break let result = loop { let x = get_value(); if x > 10 { break x; } }; // while let mut i = 0; while i < 5 { i += 1; } // for β most idiomatic for n in 1..=5 { // inclusive println!("{n}"); } for n in 0..5 { // exclusive (0..4) print!("{n} "); }
Loop Labels & Iterators
// Named loop labels for nested breaks 'outer: for i in 0..3 { for j in 0..3 { if j == 1 { break 'outer; } } } // Iterators with enumerate let v = vec!["a", "b", "c"]; for (i, val) in v.iter().enumerate() { println!("{i}: {val}"); } // Common iterator adapters v.iter().map(|x| x.len()).collect::<Vec<_>>() v.iter().filter(|x| !x.is_empty()).count()
Traits
Define & Implement a Trait
trait Summary { // Required method fn summarise(&self) -> String; // Default method fn preview(&self) -> String { format!("{}...", self.summarise()) } } struct Article { title: String } impl Summary for Article { fn summarise(&self) -> String { self.title.clone() } }
Trait Bounds & impl Trait
// impl Trait syntax (simple) fn notify(item: &impl Summary) { println!("{}", item.summarise()); } // Trait bound syntax (generic) fn notify<T: Summary>(item: &T) { println!("{}", item.summarise()); } // Multiple bounds fn show<T: Summary + Display>(t: &T) {} // where clause (cleaner) fn show<T>(t: &T) where T: Summary + Display {}
Common standard traits:
Display, Debug, Clone, Copy, PartialEq, Iterator. Derive them with #[derive(Debug, Clone, PartialEq)] above your struct.Common Macros
// Output println!("Hello, {}!", "world"); println!("{x} {y}"); // captured vars println!("{:?}", some_struct); // Debug format eprintln!("error: {}", msg); // stderr // Collections let v = vec![1, 2, 3]; // String formatting let s = format!("Hi {}!", name); // Assertions (testing & debug) assert!(x == 5); assert_eq!(x, 5); assert_ne!(x, 0); // Panic panic!("something went wrong"); // Unimplemented placeholder todo!(); unimplemented!();
Rust Basics Mastery Checklist
| Ownership & Borrowing | Key point |
|---|---|
| Explain move semantics | one owner at a time |
| Use & for immutable borrow | many &T allowed |
| Use &mut for mutable borrow | only one &mut T |
| Know when to clone vs copy | heap vs stack types |
| Types & Patterns | Key point |
|---|---|
| Define structs with impl blocks | &self / &mut self |
| Define enums with data variants | Move(x,y), Write(String) |
| Handle Option with match / if let | Some(v) / None |
| Exhaustive match all variants | _ catch-all |
| Lifetimes & Traits | Key point |
|---|---|
| Annotate lifetimes in functions | <'a> / &'a str |
| Understand elision rules | compiler infers common cases |
| Define and implement a trait | trait T {} / impl T for S {} |
| Use #[derive] for common traits | Debug, Clone, PartialEq |
Next up β Sheet 2: Rust Error Handling Β·
Result<T,E> Β· Option chaining Β· the ? operator Β· unwrap vs expect Β· custom error types Β· panic vs recoverable errors.