Rust Basics #8 Traits and Generics: Shared Behavior in a Language Without Inheritance

4 min read

This course has accumulated a few unpaid promises: Part 3’s “types that implement the Copy trait,” Part 5’s #[derive(Debug)], Part 6’s Box<dyn Error>. They are all this part’s subject — traits. In a language without inheritance, the trait is the only device for saying “different types share the same behavior,” and combined with generics it forms the backbone of Rust abstraction.

Traits: a promise of behavior #

A trait is a list of promised behaviors. It resembles an interface, with one difference: implementations can be attached to types that already exist.

src/main.rs
trait Summary {
    fn summarize(&self) -> String;
}

struct Article { title: String, body: String }
struct Comment { author: String, text: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("[Article] {}", self.title)
    }
}

impl Summary for Comment {
    fn summarize(&self) -> String {
        format!("Comment by {}: {}", self.author, self.text)
    }
}

Articles and comments are unrelated types, but both implement Summary, so they can be handled together as “things that can be summarized.” Write a body in the trait definition and it becomes a default implementation; implementing types override only what they need.

Generics and trait bounds: any type — on conditions #

To write “a function that accepts anything summarizable,” you need generics.

src/main.rs
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

<T: Summary> is a trait bound: “T can be any type, provided it implements Summary.” What happens without the condition? Delete the bound and the compiler answers:

Compile error
error[E0599]: no method named `summarize` found for reference `&T`
  |
  = help: items from traits can only be used if the type parameter
          is bounded by the trait

In some languages’ generics (or in dynamic typing), “does this method exist” is discovered only at runtime. Rust makes a generic function declare every capability it uses as a bound. The signature becomes the documentation of its requirements. Multiple conditions stack with +, as in T: Summary + Clone.

derive: standard traits, auto-implemented #

Part 5’s #[derive(Debug)] was an instruction: auto-generate the Debug trait implementation. The frequently derived set is practically the standing prescription for struct declarations.

src/main.rs
#[derive(Debug, Clone, PartialEq)]
struct Point { x: i32, y: i32 }
  • Debug: {:?} output — the debugging basic
  • Clone: provides .clone() — Part 3’s explicit deep copy was this
  • PartialEq: the == comparison — without deriving it, == on a struct does not even compile
  • Copy: the identity of Part 3’s “types copied instead of moved.” It can only be derived when every field is Copy, so putting it on a struct containing a String is a compile error

That operators and printing are all defined through traits is Rust’s consistent structure. println!’s {} is the Display trait, the + operator is the Add trait, and Part 7’s for iteration is the Iterator trait. Every question of “can this syntax be used with this type” reduces to “does it implement this trait.”

The cost story: monomorphization and dyn #

If generics are convenient, the next question is performance. Rust generics are monomorphized: the compiler generates a dedicated version for each type actually used. If print_summary is called with Article and Comment, the compiled output contains two functions, one per type, and each call is an ordinary function call. Generics cost nothing at runtime — and Part 7’s claim that iterator chains match handwritten loops rests on the same principle.

The opposite case is “the type is decided only at runtime.” That is Part 6’s Box<dyn Error>.

src/main.rs
let items: Vec<Box<dyn Summary>> = vec![
    Box::new(Article { /* ... */ }),
    Box::new(Comment { /* ... */ }),
];
for item in &items {
    println!("{}", item.summarize());
}

dyn Summary means “something that implements Summary,” which lets articles and comments live in one Vec. The price is dynamic dispatch: finding the right summarize at runtime. The practical rule: default to generics (resolved at compile time), and reach for dyn only when different types must share one collection.

Summary #

  • A trait is a promise of behavior. It gives unrelated types a shared capability, and can be implemented for types after the fact.
  • A generic function must declare every capability it uses as a trait bound (<T: Summary>). The signature documents the requirements.
  • derive(Debug, Clone, PartialEq) is the standing prescription for structs; Copy is only possible when every field is Copy. Parts 3 and 5’s promises are settled here.
  • {} is Display, + is Add, for is Iterator — most language features reduce to trait implementations.
  • Generics monomorphize to zero runtime cost. dyn, where the type is resolved at runtime, is for mixing types in one collection.
  • The final part cashes in Part 4’s promise — lifetimes — then closes the course with modules and external crates.
X