</>
CodeLearn Pro
Start Learning Free โ†’

Rust โ€” Complete Tutorial

6 modules ยท 12 examples ยท Click โ–ถ Run Code to see the console output

๐Ÿ’ก How to use this tutorial

Read each explanation, study the code, then click โ–ถ Run Code to see the output.

Module 01

Variables & Types

Rust is statically typed โ€” every variable has a type known at compile time. Unlike most languages, variables in Rust are immutable by default. To allow a variable to change you must add the mut keyword. Rust infers types automatically so you rarely need to write them out. The compiler enforces these rules strictly, which is why Rust programs are so reliable.

Variables and Primitive Types

Declare variables with let. Add mut to make them mutable. Rust has signed integers (i8โ€“i128), unsigned integers (u8โ€“u128), floats (f32, f64), booleans, and chars. The default integer type is i32 and the default float is f64. Underscores in numbers improve readability without affecting the value.

main.rsRust
fn main() {
    // Immutable by default
    let x = 5;
    // x = 6;  โ† compile error!

    // Mutable variable
    let mut count = 0;
    count += 1;
    count += 1;
    println!("count = {count}");

    // Type annotations (usually inferred)
    let age: u32     = 28;
    let pi:  f64     = 3.14159;
    let flag: bool   = true;
    let letter: char = 'R';

    println!("age={age}, pi={pi:.2}, flag={flag}, letter={letter}");

    // Shadowing โ€” reuse a name with a new binding
    let spaces = "   ";
    let spaces = spaces.len();   // now an integer!
    println!("spaces = {spaces}");

    // Constants โ€” must be type-annotated
    const MAX_POINTS: u32 = 100_000;
    println!("max = {MAX_POINTS}");

    // Tuple
    let tup: (i32, f64, bool) = (500, 6.4, true);
    let (a, b, c) = tup;
    println!("tuple: {a}, {b}, {c}");

    // Array โ€” fixed size
    let arr = [1, 2, 3, 4, 5];
    println!("arr[2] = {}", arr[2]);
    println!("arr len = {}", arr.len());
}
โ–ถ Click Run Code to see output

Shadowing (let x = x + 1) is different from mutation (mut x; x = x + 1). Shadowing creates a completely new binding and can even change the type. This is useful when transforming a value through multiple steps โ€” each step can shadow the previous one with the same name.

Strings โ€” &str vs String

Rust has two string types. &str (string slice) is a borrowed reference to string data โ€” immutable, stored in the binary or stack. String is a heap-allocated, growable, owned string. You will use both constantly. &str for read-only views, String when you need to build or modify a string.

main.rsRust
fn main() {
    // &str โ€” string slice (borrowed, immutable)
    let hello: &str = "Hello, World!";
    println!("{hello}");
    println!("length = {}", hello.len());
    println!("contains 'World' = {}", hello.contains("World"));

    // String โ€” heap-allocated, growable
    let mut owned = String::from("Hello");
    owned.push_str(", Rust!");   // append a string slice
    owned.push('!');             // append a char
    println!("{owned}");

    // String::new() โ€” empty string
    let mut s = String::new();
    s.push_str("building");
    s.push(' ');
    s.push_str("strings");
    println!("{s}");

    // format! โ€” combine strings (no ownership issues)
    let name = "Alice";
    let greeting = format!("Hello, {}! You are learning Rust.", name);
    println!("{greeting}");

    // Useful methods
    let sentence = "  rust is awesome  ";
    println!("trimmed: '{}'",    sentence.trim());
    println!("upper: {}",        sentence.trim().to_uppercase());
    println!("replace: {}",      sentence.trim().replace("awesome", "great"));

    // Convert &str to String and back
    let s1: String = "hello".to_string();
    let s2: &str   = &s1;           // borrow as &str
    println!("s1={s1}, s2={s2}");
}
โ–ถ Click Run Code to see output

The rule of thumb: use &str for function parameters (it accepts both &str and &String), use String when you need to own or build a string. The & in &str means "reference" โ€” you are borrowing the string data without taking ownership.

Module 02

Ownership & Borrowing

Ownership is Rust's most unique and important feature โ€” it is the mechanism that allows Rust to be memory-safe without a garbage collector. The rules are: (1) each value has exactly one owner, (2) when the owner goes out of scope the value is dropped (freed), (3) ownership can be transferred (moved) or borrowed (referenced). These rules are enforced at compile time โ€” zero runtime cost.

Move Semantics and Cloning

When you assign a heap-allocated value to another variable, the first variable is "moved" โ€” it no longer owns the data. To keep both variables valid, call .clone() for a deep copy. Primitive types (i32, bool, f64, char) implement the Copy trait and are duplicated automatically โ€” no move needed.

main.rsRust
fn main() {
    // Primitives are COPIED automatically
    let x = 5;
    let y = x;      // y gets a copy โ€” x is still valid
    println!("x={x}, y={y}");

    // String is MOVED โ€” x2 is no longer valid after move
    let s1 = String::from("hello");
    let s2 = s1;    // s1 is moved into s2
    // println!("{s1}");  โ† compile error: value used after move
    println!("s2 = {s2}");

    // Clone โ€” explicit deep copy
    let s3 = String::from("world");
    let s4 = s3.clone();
    println!("s3={s3}, s4={s4}");  // both valid

    // Move into a function โ€” ownership transferred
    let s5 = String::from("Rust");
    print_string(s5);
    // println!("{s5}");  โ† compile error: s5 was moved

    // Return ownership from a function
    let s6 = give_ownership();
    println!("got: {s6}");

    // Takes and gives back ownership
    let s7 = String::from("hello");
    let s8 = takes_and_returns(s7);
    println!("s8 = {s8}");
}

fn print_string(s: String) {
    println!("inside: {s}");
}   // s is dropped here

fn give_ownership() -> String {
    String::from("owned string")
}

fn takes_and_returns(s: String) -> String {
    s   // return ownership to caller
}
โ–ถ Click Run Code to see output

Moving ownership everywhere gets tedious. That is why Rust has borrowing โ€” instead of moving ownership into a function, you pass a reference (&). The function borrows the value temporarily without taking ownership. This is by far the most common pattern in real Rust code.

References and Borrowing

A reference (&T) lets you refer to a value without taking ownership. This is called borrowing. You can have many immutable references (&T) at the same time, OR exactly one mutable reference (&mut T) โ€” never both at once. This rule prevents data races at compile time.

main.rsRust
fn main() {
    // Immutable borrow โ€” just reading
    let s1 = String::from("hello");
    let len = calculate_length(&s1);    // pass reference
    println!("'{}' has {} chars", s1, len);  // s1 still valid

    // Multiple immutable borrows โ€” all OK
    let r1 = &s1;
    let r2 = &s1;
    println!("r1={r1}, r2={r2}");

    // Mutable borrow โ€” one at a time, exclusive
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("changed: {s2}");

    // Slices โ€” borrow a portion of a collection
    let numbers = [10, 20, 30, 40, 50];
    let slice = &numbers[1..4];      // indices 1, 2, 3
    println!("slice: {:?}", slice);  // [20, 30, 40]

    // String slice
    let sentence = String::from("hello world");
    let first = first_word(&sentence);
    println!("first word: {first}");
}

fn calculate_length(s: &String) -> usize {
    s.len()  // borrowing โ€” does not take ownership
}  // s goes out of scope but value is NOT dropped

fn change(s: &mut String) {
    s.push_str(", world");
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' { return &s[0..i]; }
    }
    &s[..]
}
โ–ถ Click Run Code to see output

The borrow checker rule โ€” many readers OR one writer, never both โ€” is the same rule database systems use for read/write locks. Rust enforces this at compile time with zero overhead. If your code compiles, it is free of data races.

Module 03

Structs & Enums

Structs let you group related data under a named type. Enums define a type that can be one of several variants โ€” and each variant can carry different data. Rust's enums are algebraic data types, making them far more powerful than enums in C, Java, or TypeScript. The match expression exhaustively handles every possible variant.

Structs and impl Blocks

Define a struct with field names and types. Create an instance by filling in all fields. Add methods and associated functions (like constructors) in an impl block. &self means the method borrows the struct for reading. &mut self means it borrows for writing. Self (capital S) refers to the type itself.

main.rsRust
#[derive(Debug)]
struct Rectangle {
    width:  f64,
    height: f64,
}

impl Rectangle {
    // Associated function โ€” constructor
    fn new(width: f64, height: f64) -> Self {
        Self { width, height }
    }

    fn square(side: f64) -> Self {
        Self { width: side, height: side }
    }

    // Method โ€” borrows self immutably
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn is_square(&self) -> bool {
        (self.width - self.height).abs() < f64::EPSILON
    }

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

    // Method โ€” borrows self mutably
    fn scale(&mut self, factor: f64) {
        self.width  *= factor;
        self.height *= factor;
    }
}

fn main() {
    let mut r1 = Rectangle::new(10.0, 5.0);
    let     r2 = Rectangle::new(4.0, 3.0);
    let     sq = Rectangle::square(6.0);

    println!("r1 = {:?}", r1);
    println!("area      = {:.1}", r1.area());
    println!("perimeter = {:.1}", r1.perimeter());
    println!("is_square = {}", r1.is_square());
    println!("can_hold r2? {}", r1.can_hold(&r2));

    r1.scale(2.0);
    println!("after scale(2): {:?}", r1);
    println!("sq is_square = {}", sq.is_square());
}
โ–ถ Click Run Code to see output

#[derive(Debug)] automatically generates the {:?} formatter so you can print your struct with println!("{:?}", value). You can also use {:#?} for pretty-printed output with indentation. This is one of Rust's procedural macros โ€” it generates code for you at compile time.

Enums and Pattern Matching

Enums in Rust are algebraic data types โ€” each variant can hold different types of data. The match expression must handle every variant (exhaustive matching). Option<T> is Rust's replacement for null โ€” Some(value) when data exists, None when it does not. The ? operator propagates None automatically.

main.rsRust
#[derive(Debug)]
enum Shape {
    Circle   { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle  { base: f64,  height: f64  },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle    { radius }        => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle  { base, height }  => 0.5 * base * height,
        }
    }

    fn name(&self) -> &str {
        match self {
            Shape::Circle    { .. } => "Circle",
            Shape::Rectangle { .. } => "Rectangle",
            Shape::Triangle  { .. } => "Triangle",
        }
    }
}

fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    let shapes = vec![
        Shape::Circle    { radius: 5.0 },
        Shape::Rectangle { width: 4.0, height: 6.0 },
        Shape::Triangle  { base: 3.0,  height: 8.0 },
    ];

    for shape in &shapes {
        println!("{}: area = {:.2}", shape.name(), shape.area());
    }

    // Option โ€” no null pointers!
    for (a, b) in [(10.0, 2.0), (7.0, 0.0), (9.0, 3.0)] {
        match divide(a, b) {
            Some(r) => println!("{a}/{b} = {r:.2}"),
            None    => println!("{a}/{b} = undefined"),
        }
    }

    // if let โ€” concise single-variant match
    if let Some(result) = divide(100.0, 4.0) {
        println!("100/4 = {result}");
    }

    // unwrap_or โ€” provide a default
    let val = divide(5.0, 0.0).unwrap_or(0.0);
    println!("fallback = {val}");
}
โ–ถ Click Run Code to see output

match must be exhaustive โ€” you must handle every possible variant. The underscore _ is a catch-all: _ => println!("something else"). This exhaustiveness guarantee means the compiler will tell you if you add a new enum variant and forget to handle it somewhere โ€” impossible to forget in Rust.

Module 04

Traits & Generics

Traits define shared behaviour โ€” like interfaces in Java or Go. A type implements a trait by providing the required methods. Generics let you write code that works for any type that satisfies certain trait bounds. Rust resolves generics at compile time (monomorphisation) โ€” the compiled code is as fast as if you had written a specific version for each type.

Defining and Implementing Traits

Define a trait with the trait keyword โ€” it lists method signatures. Implement it for a type with impl TraitName for TypeName. Traits can have default method implementations that types can optionally override. The impl Trait syntax in function parameters is shorthand for a generic with a trait bound.

main.rsRust
trait Describe {
    fn describe(&self) -> String;

    // Default implementation
    fn short(&self) -> String {
        let d = self.describe();
        if d.len() > 30 {
            format!("{}...", &d[..30])
        } else {
            d
        }
    }
}

struct Book {
    title:  String,
    author: String,
    pages:  u32,
}

struct Movie {
    title:    String,
    director: String,
    year:     u16,
}

impl Describe for Book {
    fn describe(&self) -> String {
        format!("'{}' by {} ({} pages)",
            self.title, self.author, self.pages)
    }
}

impl Describe for Movie {
    fn describe(&self) -> String {
        format!("'{}' directed by {} ({})",
            self.title, self.director, self.year)
    }
}

// Generic function โ€” works for any type that implements Describe
fn print_info(item: &impl Describe) {
    println!("Full:  {}", item.describe());
    println!("Short: {}", item.short());
}

fn main() {
    let book = Book {
        title:  "The Rust Programming Language".to_string(),
        author: "Steve Klabnik".to_string(),
        pages:  526,
    };

    let movie = Movie {
        title:    "Inception".to_string(),
        director: "Christopher Nolan".to_string(),
        year:     2010,
    };

    print_info(&book);
    println!();
    print_info(&movie);

    // Vec of trait objects โ€” dynamic dispatch
    let items: Vec<Box<dyn Describe>> = vec![
        Box::new(Book  { title: "1984".into(), author: "Orwell".into(), pages: 328 }),
        Box::new(Movie { title: "Matrix".into(), director: "Wachowski".into(), year: 1999 }),
    ];

    println!();
    for item in &items {
        println!("- {}", item.short());
    }
}
โ–ถ Click Run Code to see output

impl Trait (static dispatch) and Box<dyn Trait> (dynamic dispatch) are two ways to use traits in functions. impl Trait is faster โ€” the compiler generates specific code for each type at compile time. Box<dyn Trait> allows storing different types in one Vec but has a small runtime cost for the virtual dispatch.

Generic Functions and Structs

Generic functions use type parameters (T, U, etc.) that become concrete types when the function is called. Add trait bounds with T: TraitName to specify what the type must be able to do. Multiple bounds use +. The where clause keeps complex bounds readable.

main.rsRust
use std::fmt::Display;

// Generic function with a bound โ€” T must implement PartialOrd
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    for item in list {
        if item > largest { largest = item; }
    }
    largest
}

// Multiple bounds with Display and PartialOrd
fn print_largest<T: Display + PartialOrd>(list: &[T]) {
    let result = largest(list);
    println!("largest = {result}");
}

// Generic struct
#[derive(Debug)]
struct Pair<T> {
    first:  T,
    second: T,
}

impl<T: Display + PartialOrd> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Self { first, second }
    }

    fn larger(&self) -> &T {
        if self.first >= self.second { &self.first } else { &self.second }
    }

    fn show(&self) {
        println!("({}, {}) โ†’ larger: {}", self.first, self.second, self.larger());
    }
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    print_largest(&numbers);

    let chars = vec!['y', 'm', 'a', 'q'];
    print_largest(&chars);

    let p1 = Pair::new(5, 10);
    let p2 = Pair::new("hello", "world");
    let p3 = Pair::new(3.14, 2.71);

    p1.show();
    p2.show();
    p3.show();

    // The compiler generates specific code for each type used
    // โ€” zero runtime overhead compared to hand-written versions
}
โ–ถ Click Run Code to see output

Rust generics are zero-cost because of monomorphisation โ€” the compiler creates a separate version of the function for each concrete type used. So Pair<i32> and Pair<&str> become two distinct compiled functions. This is why generic Rust code is as fast as non-generic code.

Module 05

Error Handling

Rust has no exceptions. Errors are values โ€” either Option<T> (something or nothing) or Result<T, E> (success or failure). This forces you to handle errors explicitly, which is why Rust programs rarely crash unexpectedly. The ? operator is syntactic sugar that propagates errors up the call stack โ€” it makes error handling concise without hiding it.

Result<T, E> and the ? Operator

Result<T, E> is an enum with two variants: Ok(T) for success and Err(E) for failure. Use match to handle both cases. The ? operator extracts the Ok value or returns the Err immediately from the current function โ€” it is shorthand for the common "if error, return error" pattern.

main.rsRust
use std::num::ParseIntError;

// Custom error type
#[derive(Debug)]
enum AppError {
    ParseError(ParseIntError),
    NegativeNumber(i32),
    TooBig(i32),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::ParseError(e)     => write!(f, "parse error: {e}"),
            AppError::NegativeNumber(n) => write!(f, "negative: {n}"),
            AppError::TooBig(n)         => write!(f, "too big: {n} (max 100)"),
        }
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::ParseError(e) }
}

fn parse_score(s: &str) -> Result<i32, AppError> {
    let n: i32 = s.trim().parse()?;   // ? converts ParseIntError via From
    if n < 0   { return Err(AppError::NegativeNumber(n)); }
    if n > 100 { return Err(AppError::TooBig(n)); }
    Ok(n)
}

fn main() {
    let inputs = ["85", "-5", "110", "abc", "0", "100"];

    for input in inputs {
        match parse_score(input) {
            Ok(score) => println!("OK  '{input}' -> score = {score}"),
            Err(e)    => println!("ERR '{input}' -> {e}"),
        }
    }

    // Chaining with map and and_then
    let doubled = parse_score("42")
        .map(|n| n * 2)
        .unwrap_or(0);
    println!("doubled = {doubled}");

    // unwrap_or_else
    let safe = parse_score("bad")
        .unwrap_or_else(|_| -1);
    println!("safe = {safe}");
}
โ–ถ Click Run Code to see output

The ? operator can only be used in functions that return Result or Option. In main() you can write fn main() -> Result<(), Box<dyn std::error::Error>> to use ? there too. The Box<dyn Error> is a trait object that can hold any error type โ€” useful for mixing different error types.

Option<T> โ€” Handling Missing Values

Option<T> replaces null. Some(value) means data exists. None means it does not. This forces you to handle the "no value" case explicitly โ€” the compiler will not let you use an Option<T> as if it were a T. Methods like map, and_then, unwrap_or, and filter let you transform Options without unwrapping.

main.rsRust
fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some("Alice".to_string()),
        2 => Some("Bob".to_string()),
        3 => Some("Carol".to_string()),
        _ => None,
    }
}

fn get_email(name: &str) -> Option<String> {
    match name {
        "Alice" => Some("alice@example.com".to_string()),
        "Bob"   => Some("bob@example.com".to_string()),
        _       => None,
    }
}

fn main() {
    // Basic match
    for id in [1, 2, 3, 99] {
        match find_user(id) {
            Some(name) => println!("User {id}: {name}"),
            None       => println!("User {id}: not found"),
        }
    }

    println!();

    // if let โ€” cleaner for single-arm match
    if let Some(name) = find_user(1) {
        println!("Found: {name}");
    }

    // map โ€” transform the inner value
    let upper = find_user(2).map(|n| n.to_uppercase());
    println!("upper = {:?}", upper);

    // and_then โ€” chain Options (flatMap)
    let email = find_user(1).and_then(|name| get_email(&name));
    println!("email = {:?}", email);

    let no_email = find_user(3).and_then(|name| get_email(&name));
    println!("no_email = {:?}", no_email);

    // unwrap_or โ€” provide a default
    let name = find_user(99).unwrap_or_else(|| "Guest".to_string());
    println!("name = {name}");

    // ? in a function returning Option
    fn user_email(id: u32) -> Option<String> {
        let name  = find_user(id)?;   // returns None if no user
        let email = get_email(&name)?; // returns None if no email
        Some(email)
    }

    println!("user 1 email = {:?}", user_email(1));
    println!("user 3 email = {:?}", user_email(3));
    println!("user 99 email = {:?}", user_email(99));
}
โ–ถ Click Run Code to see output

and_then (also called flatMap in other languages) is the key to chaining Options. Each step produces an Option โ€” and_then unwraps the Some to pass into the next function, or short-circuits to None if any step returns None. The ? operator inside a function that returns Option does the same thing automatically.

Module 06

Iterators & Closures

Closures are anonymous functions that can capture values from their environment. Iterators let you process sequences of values one at a time โ€” and because they are lazy, the entire chain does no work until a consuming method like collect() or sum() is called. Combining closures with iterators gives you expressive, efficient data transformation pipelines with zero extra allocation.

Closures โ€” Capturing Environment

Closures are defined with |params| body. They can capture variables from the surrounding scope โ€” by reference by default, or by value with the move keyword. Rust has three closure traits: Fn (immutable reference capture), FnMut (mutable reference capture), FnOnce (takes ownership, can only be called once).

main.rsRust
fn main() {
    // Basic closures
    let add  = |a: i32, b: i32| a + b;
    let sq   = |x: i32| x * x;
    let greet = |name: &str| format!("Hello, {name}! ๐Ÿฆ€");

    println!("{}", add(3, 4));
    println!("{}", sq(9));
    println!("{}", greet("Rust"));

    // Capture by reference (default)
    let base = 100;
    let add_base = |x: i32| x + base;    // borrows base
    println!("add_base(5) = {}", add_base(5));
    println!("base is still {base}");

    // Capture by value (move)
    let prefix = String::from("LOG: ");
    let log = move |msg: &str| format!("{prefix}{msg}");
    println!("{}", log("server started"));
    // prefix is moved into log โ€” cannot use it here

    // FnMut โ€” mutates captured variable
    let mut count = 0;
    let mut increment = || { count += 1; count };
    println!("{} {} {}", increment(), increment(), increment());

    // Higher-order functions
    fn apply<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 { f(val) }
    fn apply_twice<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 { f(f(val)) }

    println!("apply sq(5)        = {}", apply(sq, 5));
    println!("apply_twice +3 (1) = {}", apply_twice(|x| x + 3, 1));

    // Returning a closure from a function
    fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
        move |x| x + n
    }
    let add5  = make_adder(5);
    let add10 = make_adder(10);
    println!("add5(3) = {}, add10(3) = {}", add5(3), add10(3));
}
โ–ถ Click Run Code to see output

The move keyword is essential when you spawn threads or return closures โ€” it ensures the closure owns its captured data so it can live beyond the current scope. Without move, a closure that captures a reference cannot outlive the referenced value.

Iterator Adaptors โ€” Lazy Pipelines

Iterator adaptors like map, filter, flat_map, and take are lazy โ€” they do nothing until a consuming method (collect, sum, for_each, count, find) is called. This means you can chain many operations and Rust will execute them in a single pass, as efficiently as a hand-written loop.

main.rsRust
fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // map โ€” transform each element
    let squares: Vec<i32> = numbers.iter().map(|&x| x * x).collect();
    println!("squares: {:?}", squares);

    // filter โ€” keep matching elements
    let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
    println!("evens:   {:?}", evens);

    // chain them โ€” odd squares
    let odd_sq: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 != 0)
        .map(|&x| x * x)
        .collect();
    println!("odd_sq:  {:?}", odd_sq);

    // fold โ€” combine all into one value
    let sum = numbers.iter().fold(0, |acc, &x| acc + x);
    println!("sum = {sum}");

    // sum, product, min, max shortcuts
    let total: i32 = numbers.iter().sum();
    let product: i32 = (1..=5).product();
    println!("total={total}, 5!={product}");

    // zip โ€” combine two iterators
    let langs  = vec!["Rust", "Go", "C++"];
    let years  = vec![2015,   2009,  1985 ];
    let zipped: Vec<_> = langs.iter().zip(years.iter()).collect();
    for (lang, year) in &zipped {
        println!("  {lang} ({year})");
    }

    // flat_map โ€” flatten nested iterators
    let words = vec!["hello world", "foo bar"];
    let all: Vec<&str> = words.iter()
        .flat_map(|s| s.split_whitespace())
        .collect();
    println!("words: {:?}", all);

    // Infinite iterator with take
    let first5_sq: Vec<i32> = (1..).map(|x| x * x).take(5).collect();
    println!("first 5 squares: {:?}", first5_sq);

    // any, all, find
    println!("any > 9?  {}", numbers.iter().any(|&x| x > 9));
    println!("all > 0?  {}", numbers.iter().all(|&x| x > 0));
    println!("first > 7: {:?}", numbers.iter().find(|&&x| x > 7));
}
โ–ถ Click Run Code to see output

Iterator chains are compiled into a single tight loop โ€” there is no intermediate allocation for each step. This is the zero-cost abstraction in action: high-level functional code with the same machine-code output as a hand-written for loop. Use iterators freely โ€” they are not slower than loops.

You finished the Rust tutorial!

You now understand Rust's ownership model, structs, enums, traits, generics, error handling, and iterators โ€” the foundation of real-world Rust development.