Once you've carved code into modules, you quickly meet scenarios where different types should share the same behavior. Rust's generics and traits are the default tools for that job. In this chapter we'll write generic functions and structs, define shared behavior with traits, and read signatures that use [[impl|impl]] Trait and trait bounds.
When Generics Become Necessary
The following function only works for i32.
fn pick_max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
To reuse the same logic for strings or custom types, you generalize the function.
fn pick_max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
Tis a type parameter.T: PartialOrdis a trait bound stating thatTmust support comparisons.- The compiler monomorphizes the function per concrete type, so the abstraction costs no runtime overhead.
Putting Generics on Structs and Enums
struct Pair<T> {
left: T,
right: T,
}
impl<T> Pair<T> {
fn new(left: T, right: T) -> Self {
Self { left, right }
}
}
impl<T: PartialOrd> Pair<T> {
fn max(&self) -> &T {
if self.left >= self.right { &self.left } else { &self.right }
}
}
impl<T> Pair<T>applies to everyT.- [[impl|impl]]:
PartialOrd>block. Use a separate impl block when additional constraints are needed. That keepsnewavailable to all types while gatingmaxbehind comparability. enum Option<T>is the textbook example of a generic enum.
Defining Behavior with Traits
Traits describe which methods a type promises to provide.
trait Printable {
fn format(&self) -> String;
}
struct User {
name: String,
}
impl Printable for User {
fn format(&self) -> String {
format!("User: {}", self.name)
}
}
fn print_item(item: &impl Printable) {
println!("{}", item.format());
}
impl Printable for Userdeclares thatUsersatisfies the trait.&impl Printableaccepts any type whose reference implementsPrintable.- You can write the equivalent signature as
fn print_item<T: Printable>(item: &T).
A simple mental model:
impl Traitkeeps signatures short.T: Traitpluswhereshines when constraints pile up or multiple types interact.
Reading Trait Bounds
fn log_all<T, I>(iter: I)
where
T: Printable,
I: IntoIterator<Item = T>,
{
for item in iter {
println!("{}", item.format());
}
}
whereclauses keep complex bounds readable.IntoIterator<Item = T>means "when iter is consumed, it yields values of typeT."- Explicit bounds prevent unwanted types from crossing module boundaries later.
Default Methods and Associated Types
Traits can set defaults and introduce placeholders that implementers fill in.
trait Repository {
type Item;
fn find_all(&self) -> Vec<Self::Item>;
fn count(&self) -> usize {
self.find_all().len()
}
}
type Item;is an associated type. Implementations pick whatItemmeans for them.- Think of associated types as "this trait implementation always works with this specific type," in contrast to generics that vary per call site.
- Default methods require no extra code unless you override them.
Blanket impls and Standard Traits
- A blanket impl such as
impl<T: Display> Printable for T { ... }adds behavior to every type that meets the bound. - Knowing the standard traits (
Display,Debug,Default,Iterator, …) gives you a template for what good APIs look like when you design your own traits.
Practice Exercises
- Create a
Config<T>struct and only offer theduplicatemethod whenT: Clone. - Define a
trait Cacheand implement it for both an in-memory and a file-backed cache so they share one interface. - Implement
impl<T: Printable> Printable for Vec<T>and reflect on the pros and cons of blanket impls.
Completion Checklist
- Can write generic functions and the trait bounds they require.
- Can use traits to set reuse boundaries in your code.
- Can read
impl Traitandwhereclauses and explain what they demand.
Next up: we'll layer lifetimes on top of these abstractions so reference-heavy functions stay safe.
💬 댓글
이 글에 대한 의견을 남겨주세요