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