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.
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
| Keyword | Meaning |
let | Create a value (can't change) |
let mut | Create a value (can change) |
const | Fixed value, known when building. Must say the type |
static | Global value, stays in same place in memory |
Primitive Types
| Type | Example | Notes |
i8/i16/i32/i64/i128/isize | let x: i32 = 42; | Whole numbers (positive & negative). Default: i32 |
u8/u16/u32/u64/u128/usize | let x: u8 = 255; | Whole numbers (positive only) |
f32/f64 | let x: f64 = 3.14; | Decimal numbers. Default: f64 |
bool | let b = true; | true or false |
char | let c = '😎'; | A single character (any language, emoji) |
&str | let s = "hi"; | A borrowed view of text (read-only) |
String | let 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
&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
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│ }
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 {}
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 │ }
match checks every possibility
7 _ catches anything else
12-15 unpack tuples in match arms
19 if let = match one case concisely
Structs
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 │ }
{ field: value }
22 ..u copies rest from u
25-27 shorthand: name = name: name
Enums
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 │ }
::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();
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
letcreates a value (can't change),let mutallows changes,constis a fixed value known when building- Default whole number:
i32, default decimal:f64 - Last thing in a block is the return value (no semicolon)
matchmust cover every possible value- Use
if letwhen checking one case impladds functions to structs/enums