Rust Basics

New to Rust? Start here. Everything you type in Rust goes inside fn main() {} — that's your program's front door.

Hello World

fn main() {
    println!("Hello, world!");
}

Run with: rustc main.rs && ./main  or  cargo run

Variables

Can't change by default. Use mut to allow changes.

Why "can't change" by default? Think of let as a signed contract — you promise what a value is. If you write let x = 5, x is 5 forever in that scope. Use mut when you actually need the whiteboard, not the contract. This prevents accidental changes and makes code easier to follow.
1 │ let x = 5;          // immutable
2 │ let mut y = 10;     // mutable
3 │ y += 1;
4 │
5 │ // Shadowing
6 │ let z = "hello";
7 │ let z = z.len();   // shadows previous z as usize
Legend: 1 can't change   2 can change   7 new let replaces old z
KeywordMeaning
letCreate a value (can't change)
let mutCreate a value (can change)
constFixed value, known when building. Must say the type
staticGlobal value, stays in same place in memory

Primitive Types

TypeExampleNotes
i8/i16/i32/i64/i128/isizelet x: i32 = 42;Whole numbers (positive & negative). Default: i32
u8/u16/u32/u64/u128/usizelet x: u8 = 255;Whole numbers (positive only)
f32/f64let x: f64 = 3.14;Decimal numbers. Default: f64
boollet b = true;true or false
charlet c = '😎';A single character (any language, emoji)
&strlet s = "hi";A borrowed view of text (read-only)
Stringlet s = "hi".to_string();Text you own (can change, grow)

Compound Types

Tuple

let t: (i32, &str, f64) = (42, "hello", 3.14);
let a = t.0;        // access by index
let (x, y, z) = t;  // unpack

Array

let arr: [i32; 3] = [1, 2, 3];
let first = arr[0];
let all_zeros = [0; 5];  // [0, 0, 0, 0, 0]

Slice

A slice is a "view" into a sequence — it doesn't own the data, just borrows a window of it. &arr[1..4] takes elements at index 1, 2, 3. The & means "I'm borrowing, not owning."
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4];  // [2, 3, 4]

Functions

The last thing in a function body is the return value (no semicolon). A semicolon turns an expression into a statement — "do this, discard the result." No semicolon means "this IS the result." Think of it as: semicolon = "and then...", no semicolon = "and here's what we're giving back."
1 │ fn add(x: i32, y: i32) -> i32 {
2 │     x + y   // last expression is return (no semicolon)
3 │ }
4 │
5 │ fn greet(name: &str) -> String {
6 │     format!("Hello, {}!", name)
7 │ }
8 │
9 │ // Early return with `return`
10│ fn safe_div(a: f64, b: f64) -> f64 {
11│     if b == 0.0 { return 0.0; }
12│     a / b
13│ }
Legend: 1 params in parens, return type after →   2 no semicolon = return this   11 return for early exit

Control Flow

if works like other languages, but if is an expression — it returns a value. Same with loop and match. This means you can write let x = if cond { 1 } else { 2 };. Semicolons throw away the value; no semicolon means "this is the value."
 1 │ // if / else
 2 │ if x > 0 {
 3 │     "positive"
 4 │ } else if x < 0 {
 5 │     "negative"
 6 │ } else {
 7 │     "zero"
 8 │ }
 9 │
10 │ // loop (infinite, break with value)
11 │ let result = loop {
12 │     break 42;
13 │ };
14 │
15 │ // while
16 │ while n > 0 {
17 │     n -= 1;
18 │ }
19 │
20 │ // for (range)
21 │ for i in 0..5 {}      // 0, 1, 2, 3, 4
22 │ for i in 0..=5 {}     // 0, 1, 2, 3, 4, 5
23 │
24 │ // for (iterator)
25 │ for item in &collection {}
Legend: 2-8 if/else is an expression (returns a value)   11-13 loop can break with a value   21 0..5 excludes 5, 0..=5 includes 5

Pattern Matching

match checks every possible value. The compiler forces you to write code for each one. Miss one? Won't build. This means you can never forget to handle None or an error. The _ catches anything you don't care about.
 1 │ let x = 2;
 2 │
 3 │ match x {
 4 │     1 => println!("one"),
 5 │     2 | 3 => println!("two or three"),
 6 │     4..=10 => println!("range"),
 7 │     _ => println!("anything else"),
 8 │ }
 9 │
10 │ // match with unpacking
11 │ let pair = (0, -5);
12 │ match pair {
13 │     (0, y) => println!("x=0, y={y}"),
14 │     (x, 0) => println!("y=0, x={x}"),
15 │     _ => (),
16 │ }
17 │
18 │ // if let (concise match for one arm)
19 │ if let Some(val) = optional {
20 │     println!("{val}");
21 │ }
Legend: 3 match checks every possibility   7 _ catches anything else   12-15 unpack tuples in match arms   19 if let = match one case concisely

Structs

A struct groups values together. Named fields (user.name) are clearer than tuple fields (user.0). Tuple structs are for simple wrappers. Unit structs are for marking types.
 1 │ struct User {
 2 │     name: String,
 3 │     age: u32,
 4 │     active: bool,
 5 │ }
 6 │
 7 │ // Tuple struct
 8 │ struct Point(i32, i32, i32);
 9 │
10 │ // Unit struct (no fields)
11 │ struct Marker;
12 │
13 │ // Create
14 │ let u = User { name: "Bob".to_string(), age: 30, active: true };
15 │ let p = Point(0, 0, 0);
16 │
17 │ // Access
18 │ u.name;
19 │ p.0;
20 │
21 │ // Update syntax
22 │ let u2 = User { name: "Alice".to_string(), ..u };
23 │
24 │ // Field init shorthand
25 │ fn new_user(name: String) -> User {
26 │     User { name, age: 0, active: true }
27 │ }
Legend: 1-5 named fields (most common)   8 tuple struct (fields by position)   14 create with { field: value }   22 ..u copies rest from u   25-27 shorthand: name = name: name

Enums

An enum says "a value is one of these things." Think of it as a menu — your value picks one option. Each option can carry different data. Option<T> is the most common enum: it's either Some(value) or None. The compiler checks that you handle None — null pointer exceptions don't exist in Rust.
 1 │ enum Direction {
 2 │     Up,
 3 │     Down,
 4 │     Left,
 5 │     Right,
 6 │ }
 7 │
 8 │ enum Message {
 9 │     Quit,
10 │     Move { x: i32, y: i32 },
11 │     Write(String),
12 │     ChangeColor(u8, u8, u8),
13 │ }
14 │
15 │ // Match on enums
16 │ let msg = Message::Move { x: 10, y: 5 };
17 │ match msg {
18 │     Message::Quit => println!("quit"),
19 │     Message::Move { x, y } => println!("move to {x},{y}"),
20 │     Message::Write(s) => println!("{s}"),
21 │     _ => (),
22 │ }
Legend: 1-6 simple variants (no data)   8-13 each variant carries different data   16 create with ::Variant   17-22 match must handle every variant

Methods (impl)

impl adds functions to a type. If it takes &self, you call it with value.method(). If it takes no self, you call it with Type::function() (like String::from()).
 1 │ struct Circle { radius: f64 }
 2 │
 3 │ impl Circle {
 4 │     // Method (takes &self)
 5 │     fn area(&self) -> f64 {
 6 │         3.14159 * self.radius * self.radius
 7 │     }
 8 │
 9 │     // Associated function (no self)
10 │     fn unit() -> Circle {
11 │         Circle { radius: 1.0 }
12 │     }
13 │ }
14 │
15 │ let c = Circle { radius: 2.0 };
16 │ c.area();
17 │ let u = Circle::unit();
Legend: 3 impl adds functions to struct   5 &self = method (called with .)   10 no self = associated fn (called with ::)   16-17 . for methods, :: for associated

println! / format!

println!("Hello");                       // no args
println!("x = {}", x);                   // single
println!("{name} is {age}", name="Bob", age=42);
println!("{0} {1} {0}", "a", "b");      // positional
println!("{:?}", vec![1, 2, 3]);         // Debug
println!("{:#?}", vec![1, 2, 3]);        // Pretty Debug
println!("{:.2}", 3.14159);              // 2 decimal places

// Debug trait required for {:?}
// Derive it: #[derive(Debug)]

Try It Yourself

Task 1: Even or Odd

Write a function that takes an i32 and returns "even" or "odd". Then call it in main and print the result.

fn even_or_odd(n: i32) -> &'static str {
    // your code here (hint: n % 2)
}

fn main() {
    // print whether 7 is even or odd
}
Show solution
fn even_or_odd(n: i32) -> &'static str {
    if n % 2 == 0 { "even" } else { "odd" }
}

fn main() {
    println!("7 is {}", even_or_odd(7));
}

Task 2: FizzBuzz

Print numbers 1 to 20. For multiples of 3 print "Fizz", for 5 print "Buzz", for both print "FizzBuzz". Use a for loop and if/else.

fn main() {
    for n in 1..=20 {
        // your code here
    }
}
Show solution
fn main() {
    for n in 1..=20 {
        if n % 3 == 0 && n % 5 == 0 {
            println!("FizzBuzz");
        } else if n % 3 == 0 {
            println!("Fizz");
        } else if n % 5 == 0 {
            println!("Buzz");
        } else {
            println!("{n}");
        }
    }
}

Task 3: Rectangle Area

Create a Rectangle struct with width and height. Add an area method and a can_hold method that checks if this rect can hold another rect.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        // your code
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        // your code
    }
}
Show solution
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width >= other.width && self.height >= other.height
    }
}

fn main() {
    let r1 = Rectangle { width: 10, height: 5 };
    let r2 = Rectangle { width: 8, height: 3 };
    println!("Area: {}", r1.area());
    println!("Can hold: {}", r1.can_hold(&r2));
}

Task 4: Calculator Enum

Create an Operation enum with Add, Subtract, Multiply, Divide. Write a calculate function that takes two f64 values and an Operation, and returns a f64. Use match.

enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

fn calculate(a: f64, b: f64, op: Operation) -> f64 {
    // your code here
}

fn main() {
    let result = calculate(10.0, 3.0, Operation::Divide);
    println!("10 / 3 = {result}");
}
Show solution
fn calculate(a: f64, b: f64, op: Operation) -> f64 {
    match op {
        Operation::Add => a + b,
        Operation::Subtract => a - b,
        Operation::Multiply => a * b,
        Operation::Divide => a / b,
    }
}

Summary