Rust Mid-Level
The working developer's toolkit — traits, generics, lifetimes, iterators, closures, modules, and smart pointers.
Traits
01Traits 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
02Generics 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
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
03Lifetimes 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
- Each input reference gets its own lifetime parameter.
- If there is exactly one input lifetime, it's assigned to all outputs.
- If there are multiple input lifetimes but one is
&self, the output gets self's lifetime.
Iterators
04Iterators 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}");
}
| Adapter | Does |
|---|---|
map | Transform each element |
filter | Keep elements matching predicate |
take(n) / skip(n) | First n / drop n |
chain | Concatenate two iterators |
zip | Pair elements from two iterators |
fold | Accumulate to single value |
collect | Drain into a Vec / HashMap / String |
Closures
05Anonymous functions that can capture variables from their environment. They implement one of three traits depending on how they capture:
| Trait | Captures | Use |
|---|---|---|
Fn | By reference | Read-only, callable many times |
FnMut | By mutable reference | Mutates captured state |
FnOnce | By move | Consumes 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
06Code is organised into modules. Items are private by default; expose them with pub. Use use to bring names into scope.
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
}
}
}
use mycrate::math::{add, stats::mean};
fn main() {
println!("{}", add(2, 3));
println!("{}", mean(&[1.0, 2.0, 3.0]));
}
Smart Pointers
07| Type | Purpose |
|---|---|
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.