Level 2 · Mid

Rust Mid-Level

The working developer's toolkit — traits, generics, lifetimes, iterators, closures, modules, and smart pointers.

Traits Trait Objects Generics Lifetimes Iterators Closures Modules Box/Rc/RefCell

Traits

01

Traits define shared behaviour — like interfaces in other languages, but with default methods, associated types, and generics. Implementing a trait gives a type new capabilities.

trait Greet {
    fn hello(&self) -> String;

    // Default method — types can override
    fn loud_hello(&self) -> String {
        format!("{}!!!", self.hello())
    }
}

struct French;
struct English;

impl Greet for French {
    fn hello(&self) -> String { "Bonjour".into() }
}

impl Greet for English {
    fn hello(&self) -> String { "Hello".into() }
}

println!("{}", French.loud_hello());   // "Bonjour!!!"
println!("{}", English.hello());        // "Hello"

Trait Objects (dyn)

Use Box<dyn Trait> or &dyn Trait for runtime polymorphism — useful when you need a collection of heterogeneous types.

let greeters: Vec<Box<dyn Greet>> = vec![
    Box::new(French),
    Box::new(English),
];

for g in &greeters {
    println!("{}", g.hello());
}

Generics

02

Generics let one function or type work over many concrete types. Constrain them with trait bounds.

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut best = &list[0];
    for item in list {
        if item > best { best = item; }
    }
    best
}

let nums   = vec![34, 50, 25, 100, 65];
let chars  = vec!['y', 'm', 'a', 'q'];
println!("{}", largest(&nums));     // 100
println!("{}", largest(&chars));    // y
Generic struct with multiple bounds
use std::fmt::Display;
use std::ops::Add;

struct Pair<T: Display + Add<Output = T> + Copy> {
    a: T,
    b: T,
}

impl<T: Display + Add<Output = T> + Copy> Pair<T> {
    fn sum(&self) -> T { self.a + self.b }
    fn show(&self) { println!("{} + {} = {}", self.a, self.b, self.sum()); }
}

Pair { a: 3, b: 4 }.show();      // "3 + 4 = 7"

Lifetimes

03

Lifetimes tell the compiler how long references are valid. Most are inferred (elision); you only name them when the compiler can't figure it out — usually when a function returns a reference that depends on multiple inputs.

// 'a says: the returned reference lives as long as the shorter of x and y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

let s1 = String::from("long string");
let result;
{
    let s2 = String::from("short");
    result = longest(&s1, &s2);
    println!("{result}");    // ✅ result borrowed inside the inner scope
}
// println!("{result}");      // ❌ s2 dropped — result would dangle

Lifetime elision rules

  1. Each input reference gets its own lifetime parameter.
  2. If there is exactly one input lifetime, it's assigned to all outputs.
  3. If there are multiple input lifetimes but one is &self, the output gets self's lifetime.

Iterators

04

Iterators are lazy — adapters like map / filter build a pipeline; a consumer like collect / sum drives execution. Compiled output is as fast as a hand-written loop.

let nums = vec![1, 2, 3, 4, 5, 6];

let evens_sq: Vec<i32> = nums.iter()
    .filter(|n| **n % 2 == 0)
    .map(|n| n * n)
    .collect();
// [4, 16, 36]

let total: i32 = nums.iter().sum();
let max     = nums.iter().max();
let count   = nums.iter().filter(|n| **n > 3).count();

// Zip + enumerate
let names = ["a", "b", "c"];
for (i, name) in names.iter().enumerate() {
    println!("{i}: {name}");
}
AdapterDoes
mapTransform each element
filterKeep elements matching predicate
take(n) / skip(n)First n / drop n
chainConcatenate two iterators
zipPair elements from two iterators
foldAccumulate to single value
collectDrain into a Vec / HashMap / String

Closures

05

Anonymous functions that can capture variables from their environment. They implement one of three traits depending on how they capture:

TraitCapturesUse
FnBy referenceRead-only, callable many times
FnMutBy mutable referenceMutates captured state
FnOnceBy moveConsumes captures, single call
let factor = 10;
let multiply = |x: i32| x * factor;   // captures factor by reference (Fn)
println!("{}", multiply(5));          // 50

let mut log: Vec<String> = vec![];
let mut record = |msg: &str| log.push(msg.to_string());  // FnMut
record("first");
record("second");

let owned = String::from("bye");
let consume = move || println!("{owned}");  // FnOnce
consume();

Modules

06

Code is organised into modules. Items are private by default; expose them with pub. Use use to bring names into scope.

src/lib.rs
pub mod math {
    pub fn add(a: i32, b: i32) -> i32 { a + b }

    pub mod stats {
        pub fn mean(xs: &[f64]) -> f64 {
            xs.iter().sum::<f64>() / xs.len() as f64
        }
    }
}
src/main.rs
use mycrate::math::{add, stats::mean};

fn main() {
    println!("{}", add(2, 3));
    println!("{}", mean(&[1.0, 2.0, 3.0]));
}

Smart Pointers

07
TypePurpose
Box<T>Heap allocation. Use for recursive types or large values.
Rc<T>Single-threaded reference counting. Multiple owners.
Arc<T>Atomic Rc — safe to share across threads.
RefCell<T>Interior mutability with runtime borrow checks.
Mutex<T>Interior mutability across threads.
use std::rc::Rc;
use std::cell::RefCell;

// A graph node shared by many edges + with mutable state
struct Node { value: i32, next: Option<Rc<RefCell<Node>>> }

let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
let b = Rc::new(RefCell::new(Node { value: 2, next: Some(Rc::clone(&a)) }));

// Mutate inside the RefCell
b.borrow_mut().value = 99;

Next up

Ready for the hard stuff? Continue with Rust Advanced — async/await, Send/Sync, unsafe, macros, FFI, and performance tuning.

Articles Tags Products