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
- Closures
- Iterators & Iterator Adaptors
- Generics
- Enums & the Type System
- Smart Pointers
- 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
}
🦀 Use
movebefore 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
forloops — 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 +
matchis 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]
}
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
Rc→ArcandRefCell→Mutex.
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:
| 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)