[Rust Series Part 13] Designing Reuse with Generics and Traits

한국어 버전

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 }
}
  • T is a type parameter.
  • T: PartialOrd is a trait bound stating that T must 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 every T.
  • [[impl|impl]]: PartialOrd> block. Use a separate impl block when additional constraints are needed. That keeps new available to all types while gating max behind 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 User declares that User satisfies the trait.
  • &impl Printable accepts any type whose reference implements Printable.
  • You can write the equivalent signature as fn print_item<T: Printable>(item: &T).

A simple mental model:

  • impl Trait keeps signatures short.
  • T: Trait plus where shines 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());
    }
}
  • where clauses keep complex bounds readable.
  • IntoIterator<Item = T> means "when iter is consumed, it yields values of type T."
  • 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 what Item means 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.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Practice Exercises

  1. Create a Config<T> struct and only offer the duplicate method when T: Clone.
  2. Define a trait Cache and implement it for both an in-memory and a file-backed cache so they share one interface.
  3. 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 Trait and where clauses and explain what they demand.

Next up: we'll layer lifetimes on top of these abstractions so reference-heavy functions stay safe.

💬 댓글

이 글에 대한 의견을 남겨주세요