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 in CodeSandbox
The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.
💬 댓글
이 글에 대한 의견을 남겨주세요