DEV Community

Cover image for Advanced Rust Concepts: Iterators, Closures, Generics & More (Part 2)
mihir mohapatra
mihir mohapatra

Posted on

Advanced Rust Concepts: Iterators, Closures, Generics & More (Part 2)

This is Part 2 of the Core Rust Concepts series. If you haven't read Part 1, start there — it covers Ownership, Borrowing, Lifetimes, Traits, Result/Option, and Pattern Matching.


Table of Contents

  1. Closures
  2. Iterators & Iterator Adaptors
  3. Generics
  4. Enums & the Type System
  5. Smart Pointers
  6. Async / Await

7. Closures

Closures are anonymous functions you can store in variables or pass as arguments. Unlike regular functions, they can capture variables from the surrounding scope.

Rust has three closure traits depending on how they use the environment:

Trait Captures Can call
Fn Immutable borrow Many times
FnMut Mutable borrow Many times
FnOnce Takes ownership Once only
fn apply<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 {
    f(val)
}

fn main() {
    let factor = 3;
    let multiply = |x| x * factor; // captures `factor` from scope

    println!("{}", apply(multiply, 5)); // 15

    // FnOnce: consumes the captured value
    let s = String::from("hello");
    let consume = move || println!("{}", s); // takes ownership of s
    consume();
    // consume(); ← error: already moved
}
Enter fullscreen mode Exit fullscreen mode

🦀 Use move before a closure to force it to take ownership of captured variables — common in threads and async code.


8. Iterators & Iterator Adaptors

Rust's iterator system is one of its best features. Iterators are lazy — nothing executes until you call a consumer. You chain adaptor methods and the compiler optimizes the whole pipeline as if you wrote a hand-rolled loop.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6];

    // chain: filter → map → collect
    let result: Vec<i32> = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)  // keep even numbers
        .map(|x| x * 10)            // multiply by 10
        .collect();                 // [20, 40, 60]

    let sum: i32 = (1..=100).sum(); // 5050 — no loop needed!

    // enumerate + take
    for (i, val) in numbers.iter().enumerate().take(3) {
        println!("[{i}] = {val}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Adaptors (lazy — don't run yet):

  • map, filter, flat_map
  • take, skip, enumerate
  • zip, chain, peekable

Consumers (trigger evaluation):

  • collect, sum, product
  • count, max, min
  • find, any, all, fold

💡 Prefer iterator chains over manual for loops — they're often faster, always more expressive, and the compiler can auto-vectorize them.


9. Generics

Generics let you write code that works for any type, with zero runtime overhead. Rust uses monomorphization — at compile time it generates a concrete version for each type you actually use. No boxing, no vtable lookup.

// Generic struct with trait bounds
struct Pair<T> {
    first: T,
    second: T,
}

impl<T: std::fmt::Display + PartialOrd> Pair<T> {
    fn larger(&self) -> &T {
        if self.first > self.second { &self.first }
        else { &self.second }
    }
}

fn main() {
    let nums = Pair { first: 5, second: 10 };
    let strs = Pair { first: "apple", second: "banana" };

    println!("{}", nums.larger());  // 10
    println!("{}", strs.larger()); // "banana"
}
Enter fullscreen mode Exit fullscreen mode

Trait bounds (the T: Display + PartialOrd part) let you constrain what types are allowed. You can also use the where clause for readability:

fn print_larger<T>(a: T, b: T)
where
    T: std::fmt::Display + PartialOrd,
{
    if a > b { println!("{a}") } else { println!("{b}") }
}
Enter fullscreen mode Exit fullscreen mode

10. Enums & the Type System

Rust's enum is far more powerful than in most languages — each variant can hold different data. This makes them algebraic data types, great for modelling state machines, ASTs, or any "one of these shapes" scenario.

enum Shape {
    Circle(f64),                          // radius
    Rectangle(f64, f64),                  // width, height
    Triangle { base: f64, height: f64 },  // named fields
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r)               => 3.14159 * r * r,
            Shape::Rectangle(w, h)         => w * h,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

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

    for s in &shapes {
        println!("area: {:.2}", s.area());
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

area: 78.54
area: 12.00
area: 24.00
Enter fullscreen mode Exit fullscreen mode

Enums + match is how Rust replaces inheritance. No subclassing needed — just model your variants explicitly and let the compiler verify you handle all of them.


11. Smart Pointers

When the ownership rules feel too strict, smart pointers offer controlled escape hatches — each with clearly defined trade-offs.

Type Purpose Thread-safe?
Box<T> Heap allocation, single owner Yes
Rc<T> Shared ownership, ref-counted No
Arc<T> Shared ownership, atomic ref-count Yes
RefCell<T> Interior mutability, runtime borrow check No
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    // Box: heap-allocate a value
    let boxed = Box::new(42);
    println!("{}", *boxed); // 42

    // Rc: multiple owners (single-threaded)
    let shared = Rc::new(String::from("shared"));
    let clone1 = Rc::clone(&shared);
    println!("ref count: {}", Rc::strong_count(&shared)); // 2
    drop(clone1);
    println!("ref count: {}", Rc::strong_count(&shared)); // 1

    // RefCell: mutate through a shared reference
    let data = RefCell::new(vec![1, 2, 3]);
    data.borrow_mut().push(4);
    println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}
Enter fullscreen mode Exit fullscreen mode

Common pattern — Rc<RefCell<T>> gives you multiple owners that can all mutate the data:

use std::rc::Rc;
use std::cell::RefCell;

let shared_vec = Rc::new(RefCell::new(vec![1, 2]));
let a = Rc::clone(&shared_vec);
let b = Rc::clone(&shared_vec);

a.borrow_mut().push(3);
b.borrow_mut().push(4);

println!("{:?}", shared_vec.borrow()); // [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

For multi-threaded code, swap RcArc and RefCellMutex.


12. Async / Await

Rust's async model is built on futures — values that represent work not yet completed. Marking a function async makes it return a Future. You use .await to yield control until it resolves. A runtime like tokio drives the futures.

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
Enter fullscreen mode Exit fullscreen mode
use tokio::time::{sleep, Duration};

async fn fetch_data(id: u32) -> String {
    sleep(Duration::from_millis(100)).await;
    format!("data for id={id}")
}

#[tokio::main]
async fn main() {
    // Sequential — takes ~200ms
    // let a = fetch_data(1).await;
    // let b = fetch_data(2).await;

    // Concurrent — takes ~100ms
    let (a, b) = tokio::join!(
        fetch_data(1),
        fetch_data(2),
    );

    println!("{}", a);
    println!("{}", b);
}
Enter fullscreen mode Exit fullscreen mode

Key async concepts:

// Spawn a task (like a lightweight thread)
tokio::spawn(async {
    println!("running concurrently");
});

// Select: race multiple futures, use whichever finishes first
tokio::select! {
    result = fetch_data(1) => println!("got: {result}"),
    _ = sleep(Duration::from_secs(1)) => println!("timed out"),
}
Enter fullscreen mode Exit fullscreen mode

💡 Unlike Go or Erlang, Rust async has zero runtime overhead — futures compile down to state machines with no hidden heap allocations per task.


Wrapping Up

Here's a quick summary of what Part 2 covered:

Concept What it gives you
Closures Capture-by-reference or by-value anonymous functions
Iterators Lazy, composable, zero-cost data pipelines
Generics Monomorphized, compile-time polymorphism
Enums (ADTs) Model complex state without inheritance
Smart Pointers Heap, shared ownership, and interior mutability
Async/Await Non-blocking concurrency with tokio

What's next?

Part 3 will cover:

  • Macros (macro_rules! and proc macros)
  • Modules and the Cargo ecosystem
  • Testing in Rust
  • Unsafe Rust and FFI

Found this useful? Drop a ❤️ and follow for Part 3!

Top comments (0)