Advanced Rust Concepts: Iterators, Closures, Generics & More…

Advanced Rust Concepts: Iterators, Closures, Generics & More…

DEV Community: rust (mihir mohapatra)

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:


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
}

🦀 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}");
}
}

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"
}

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}") }
}

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());
}
}

Output:

area: 78.54
area: 12.00
area: 24.00

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.


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]
}

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]

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"] }

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);
}

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"),
}

💡 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:


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!

Generated by RSStT. The copyright belongs to the original author.

Source

Report Page