With the fundamentals of ownership, borrowing, and code structure under your belt, you're ready for Rust's powerful abstractions: generics for code reuse, traits for shared behavior, smart pointers for flexible memory management, closures for functional programming, and iterators for elegant data processing.
Rust's advanced features follow the zero-cost abstraction principle: you don't pay runtime costs for features you don't use, and the features you do use are as efficient as hand-written code. The compiler optimizes abstractions away entirely.
Generics let you write code that works with multiple types, eliminating duplication while maintaining type safety:
// Without generics - duplicated code
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
// With generics - single implementation
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars));
}
// Single type parameter
struct Point<T> {
x: T,
y: T,
}
// Multiple type parameters
struct Pair<T, U> {
first: T,
second: U,
}
fn main() {
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
let mixed_pair = Pair { first: 5, second: 4.0 };
}
// Generic enums (you've already used these!)
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
// Method only for specific types
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
// Different generic parameters in method
impl<T, U> Pair<T, U> {
fn mixup<V, W>(self, other: Pair<V, W>) -> Pair<T, W> {
Pair {
first: self.first,
second: other.second,
}
}
}
Generics have zero runtime cost. The compiler generates specialized code for each concrete type used through a process called monomorphization. Point<i32> and Point<f64> become separate types at compile time, with no runtime dispatch overhead.
Traits define shared behavior that types can implement. They're similar to interfaces in other languages but more powerful:
// Define a trait
trait Summary {
fn summarize(&self) -> String;
// Default implementation (optional)
fn read_more(&self) -> String {
String::from("(Read more...)")
}
}
// Implement for a type
struct Article {
headline: String,
author: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
// Uses default read_more()
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.username, self.content)
}
// Override default
fn read_more(&self) -> String {
format!("Follow @{} for more", self.username)
}
}
fn main() {
let article = Article {
headline: String::from("Rust is awesome"),
author: String::from("rustacean"),
content: String::from("..."),
};
println!("{}", article.summarize());
}
// Require types implement a trait
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// Multiple bounds with +
fn display_summary<T: Summary + Display>(item: &T) {
println!("{}: {}", item, item.summarize());
}
// Where clause for cleaner complex bounds
fn complex_function<T, U>(t: &T, u: &U) -> String
where
T: Summary + Clone,
U: Display + Debug,
{
format!("{}: {:?}", t.summarize(), u)
}
// impl Trait syntax (shorthand for simple cases)
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("rust_lang"),
content: String::from("Hello Rustaceans!"),
}
}
| Trait | Purpose | Methods |
|---|---|---|
Clone |
Explicit deep copy | .clone() |
Copy |
Implicit bitwise copy (stack types) | automatic |
Debug |
Developer-facing format | {:?} |
Display |
User-facing format | {} |
Default |
Create default value | Default::default() |
PartialEq/Eq |
Equality comparison | ==, != |
PartialOrd/Ord |
Ordering comparison | <, >, <=, >= |
From/Into |
Type conversions | .into() |
Smart pointers are data structures that act like pointers but have additional metadata and capabilities. They implement the Deref and Drop traits.
// Store data on the heap
let b = Box::new(5);
println!("b = {}", b); // Automatic deref
// Common use: recursive types
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
// Also useful for large data
struct LargeData { data: [u8; 1000000] }
let boxed = Box::new(LargeData { data: [0; 1000000] }); // Only pointer on stack
use std::rc::Rc;
// Multiple owners of the same data
let a = Rc::new(5);
let b = Rc::clone(&a); // Increases reference count
let c = Rc::clone(&a); // Another clone
println!("Reference count: {}", Rc::strong_count(&a)); // 3
// When all Rc's go out of scope, data is dropped
{
let d = Rc::clone(&a);
println!("Count inside: {}", Rc::strong_count(&a)); // 4
}
println!("Count after: {}", Rc::strong_count(&a)); // 3
Rc<T> is not thread-safe. For multi-threaded scenarios, use Arc<T> (Atomic Reference Counting) instead, which we'll cover in Chapter 7.
use std::cell::RefCell;
// Mutate inside an immutable container
let data = RefCell::new(5);
// Borrow mutably at runtime
*data.borrow_mut() += 1;
println!("{}", data.borrow()); // 6
// Common pattern: Rc + RefCell for shared mutable data
use std::rc::Rc;
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
clone1.borrow_mut().push(4);
clone2.borrow_mut().push(5);
println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
RefCell moves borrowing checks from compile time to runtime. Violating the borrowing rules (e.g., two mutable borrows) will cause a panic, not a compile error. Use sparingly and only when necessary.
Closures are anonymous functions that can capture their environment. They're essential for iterators, callbacks, and functional programming patterns:
let x = 4;
// Closure that captures x from environment
let equal_to_x = |z| z == x;
println!("{}", equal_to_x(4)); // true
// With explicit type annotations
let add_one = |x: i32| -> i32 { x + 1 };
// Multiline closure
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
std::thread::sleep(std::time::Duration::from_secs(2));
num * 2
};
// Type inference works for closures
let square = |x| x * x;
let result = square(5); // Infers i32
// Borrow immutably (default when possible)
let list = vec![1, 2, 3];
let only_borrows = || println!("list: {:?}", list);
only_borrows();
println!("After: {:?}", list); // list still usable
// Borrow mutably
let mut list = vec![1, 2, 3];
let mut borrows_mutably = || list.push(7);
borrows_mutably();
// Take ownership with move
let list = vec![1, 2, 3];
let takes_ownership = move || println!("{:?}", list);
takes_ownership();
// println!("{:?}", list); // ERROR: list was moved
| Trait | Captures | Can Call |
|---|---|---|
Fn |
By immutable reference | Multiple times |
FnMut |
By mutable reference | Multiple times |
FnOnce |
By value (takes ownership) | Once only |
// Accepting closures as parameters
fn apply_to_3<F>(f: F) -> i32
where
F: Fn(i32) -> i32
{
f(3)
}
let double = |x| x * 2;
println!("3 doubled: {}", apply_to_3(double)); // 6
Iterators process sequences of elements lazily and efficiently. They're a cornerstone of idiomatic Rust:
let v = vec![1, 2, 3];
// Create an iterator
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(&1)
println!("{:?}", iter.next()); // Some(&2)
println!("{:?}", iter.next()); // Some(&3)
println!("{:?}", iter.next()); // None
// for loop uses iterators implicitly
for val in v.iter() {
println!("{}", val);
}
let v = vec![1, 2, 3, 4, 5];
// Chain adapter methods (lazy evaluation!)
let result: Vec<_> = v.iter()
.filter(|x| **x > 2) // Keep only values > 2
.map(|x| x * 2) // Double each value
.collect(); // Collect into Vec
println!("{:?}", result); // [6, 8, 10]
// Nothing happens until collect() - lazy evaluation!
let numbers = vec![1, 2, 3, 4, 5];
// Aggregation
let sum: i32 = numbers.iter().sum(); // 15
let product = numbers.iter().fold(1, |acc, x| acc * x); // 120
// Searching
let has_even = numbers.iter().any(|x| x % 2 == 0); // true
let all_positive = numbers.iter().all(|x| *x > 0); // true
let first_even = numbers.iter().find(|x| *x % 2 == 0); // Some(&2)
// Transformation
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
let evens: Vec<_> = numbers.iter().filter(|x| *x % 2 == 0).collect();
// Enumeration and zipping
for (i, val) in numbers.iter().enumerate() {
println!("{}: {}", i, val);
}
let letters = vec!['a', 'b', 'c'];
for (num, letter) in numbers.iter().zip(letters.iter()) {
println!("{} -> {}", num, letter);
}
Iterators are a zero-cost abstraction in Rust. The compiler optimizes iterator chains into efficient loops, often generating the same assembly as hand-written loops. Lazy evaluation means no intermediate collections are created.
// Automatically implement traits with derive
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone, Default)]
struct Config {
debug_mode: bool,
max_connections: u32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone
println!("{:?}", p1); // Debug
println!("{}", p1 == p2); // PartialEq: true
let config = Config::default(); // Default
println!("{:?}", config);
}
Box<T> vs Rc<T>? What problem does each solve?Rust's ownership system enables safe concurrent programming: