Rust Cheatsheet

Generics

Use this Rust reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Generic Functions

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

// Multiple type parameters
fn pair<A, B>(a: A, b: B) -> (A, B) { (a, b) }

// Used just like a regular function — type inferred from args
let n = largest(&[1, 5, 3, 2, 4]);  // &i32
let s = largest(&["apple", "banana", "cherry"]);  // &&str
let p = pair(42, "hello");  // (i32, &str)

Generic Structs

struct Pair<T> {
    first: T,
    second: T,
}

struct KeyValue<K, V> {
    key: K,
    value: V,
}

// Instantiate
let p = Pair { first: 5, second: 10 };
let kv = KeyValue { key: "name", value: String::from("Alice") };

// impl for all T
impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self { Pair { first, second } }
    fn first(&self) -> &T { &self.first }
    fn second(&self) -> &T { &self.second }
}

// impl only for specific T (conditional)
impl<T: std::fmt::Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.first >= self.second {
            println!("first is larger: {}", self.first);
        } else {
            println!("second is larger: {}", self.second);
        }
    }
}

Generic Enums

// These are in std, shown here for clarity:
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

// Custom generic enum
enum Tree<T> {
    Leaf(T),
    Node { left: Box<Tree<T>>, right: Box<Tree<T>>, value: T },
}

impl<T: std::fmt::Debug> Tree<T> {
    fn new_leaf(val: T) -> Self { Tree::Leaf(val) }
}

Trait Bounds

// Single bound
fn print<T: std::fmt::Display>(val: T) {
    println!("{}", val);
}

// Multiple bounds with +
fn print_debug<T: std::fmt::Display + std::fmt::Debug>(val: T) {
    println!("{} = {:?}", val, val);
}

// where clause (cleaner for multiple/complex bounds)
fn convert<T, U>(val: T) -> U
where
    T: std::fmt::Debug + Clone,
    U: From<T>,
{
    U::from(val.clone())
}

// Bound on references
fn sum_refs<'a, T>(list: &[&'a T]) -> T
where
    T: std::iter::Sum<&'a T>,
{
    list.iter().copied().sum()
}

impl Trait Syntax

Shorter syntax for trait bounds; can use in function parameter and return position.

// Parameter position — same as T: Display
fn notify(item: &impl std::fmt::Display) {
    println!("{}", item);
}

// Return position — opaque type (static dispatch, concrete type hidden)
fn make_greeting(name: &str) -> impl std::fmt::Display {
    format!("Hello, {}!", name)
}

// Key difference from where T: — impl Trait in parameter is syntactic sugar,
// but in return position it means "some specific concrete type, not named"

Generic Trait Implementations

use std::fmt;

// Blanket impl: implement for all types satisfying bound
struct Wrapper<T>(T);

impl<T: fmt::Display> fmt::Display for Wrapper<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[{}]", self.0)
    }
}

let w = Wrapper(42);
println!("{}", w);   // [42]

let ws = Wrapper("hello");
println!("{}", ws);  // [hello]

Lifetimes as Generic Parameters

Lifetimes are a form of generic parameter — they must be declared alongside type params:

// 'a is a lifetime generic parameter
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// Mixed type and lifetime generics
fn first_or_default<'a, T: Default + Clone>(slice: &'a [T]) -> T {
    slice.first().cloned().unwrap_or_default()
}

// Struct with lifetime
struct StrPair<'a> {
    first: &'a str,
    second: &'a str,
}

impl<'a> StrPair<'a> {
    fn longest(&self) -> &'a str {
        if self.first.len() >= self.second.len() { self.first } else { self.second }
    }
}

Type Aliases for Generic Types

type BoxedFn<T> = Box<dyn Fn(T) -> T>;
type StringMap<V> = std::collections::HashMap<String, V>;
type IoResult<T> = Result<T, std::io::Error>;

fn process(f: BoxedFn<i32>, x: i32) -> i32 { f(x) }
fn read_to_map(path: &str) -> IoResult<StringMap<i32>> {
    todo!()
}

PhantomData — Zero-Sized Type Markers

Use when a generic parameter is not in any field but is needed for type-checking:

use std::marker::PhantomData;

struct Typed<T> {
    value: i64,
    _marker: PhantomData<T>,
}

struct Meters;
struct Feet;

type MetersVal = Typed<Meters>;
type FeetVal = Typed<Feet>;

impl<T> Typed<T> {
    fn new(v: i64) -> Self { Typed { value: v, _marker: PhantomData } }
}

let m = MetersVal::new(100);
let f = FeetVal::new(328);
// Can't mix them — type system enforces the unit

Const Generics

Types parameterized by constant values (e.g., array length):

// Function generic over array length
fn array_sum<const N: usize>(arr: [i32; N]) -> i32 {
    arr.iter().sum()
}

let s1 = array_sum([1, 2, 3]);       // N = 3
let s2 = array_sum([1, 2, 3, 4, 5]); // N = 5

// Struct generic over const
struct Matrix<T, const ROWS: usize, const COLS: usize> {
    data: [[T; COLS]; ROWS],
}

impl<T: Default + Copy, const R: usize, const C: usize> Matrix<T, R, C> {
    fn new() -> Self {
        Matrix { data: [[T::default(); C]; R] }
    }

    fn get(&self, row: usize, col: usize) -> &T {
        &self.data[row][col]
    }
}

let m: Matrix<f64, 3, 3> = Matrix::new();

Monomorphization

Generics in Rust use monomorphization — the compiler generates a separate concrete copy of the function for each type used. This means: - Zero runtime overhead (unlike dyn Trait which has vtable indirection). - Larger binary if many types are used.

fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T { a + b }

// The compiler generates:
// fn add_i32(a: i32, b: i32) -> i32 { a + b }
// fn add_f64(a: f64, b: f64) -> f64 { a + b }
// ... for each concrete type used

add(1i32, 2);    // calls add_i32
add(1.0f64, 2.0); // calls add_f64

Higher-Ranked Trait Bounds (HRTB)

For closures or functions that must work for any lifetime:

// `for<'a>` means "for all lifetimes 'a"
fn apply_to_str<F>(f: F) -> String
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    f("hello world").to_string()
}

let result = apply_to_str(|s| &s[..5]);  // "hello"

// Commonly seen with Fn traits in async/multi-threaded code
fn store<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> usize,
{
    println!("{}", f("test"));
}

Coherence and Orphan Rules

You can implement a trait for a type only if either the trait or the type is defined in your crate. You cannot implement foreign traits on foreign types.

// OK: your trait on foreign type
trait Pretty { fn pretty(&self) -> String; }
impl Pretty for Vec<i32> { fn pretty(&self) -> String { format!("{:?}", self) } }

// OK: foreign trait on your type
struct MyType(i32);
impl std::fmt::Display for MyType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyType({})", self.0)
    }
}

// NOT OK (compiler error — orphan rule):
// impl std::fmt::Display for Vec<i32> {}  // both foreign

Use the newtype pattern to work around orphan rules:

struct Wrapper(Vec<i32>);
impl std::fmt::Display for Wrapper {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}]", self.0.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", "))
    }
}

Common Generic Patterns

// Accept anything that can be converted to a String
fn greet(name: impl Into<String>) {
    let name = name.into();
    println!("Hello, {}!", name);
}
greet("alice");               // &str → String
greet(String::from("bob"));   // String → String (identity)

// Accept anything that can be treated as a slice
fn print_all<T: std::fmt::Debug>(items: &[T]) {
    for item in items { println!("{:?}", item); }
}
print_all(&[1, 2, 3]);
print_all(&vec!["a", "b"]);

// Accept anything iterable
fn count_items<I: IntoIterator>(iter: I) -> usize {
    iter.into_iter().count()
}
count_items(vec![1, 2, 3]);   // 3
count_items(0..10);           // 10
count_items([1, 2]);          // 2