Rust track

Lesson 14 · Part 3: Structs, Enums & Traits

Traits

The trait system, generic bounds and standard traits

Part 3: Structs, Enums & Traits6 min readFree lesson

A trait is Rust's tool for polymorphism. It is similar to an "interface" in other languages, but it can have default methods and supports both compile-time monomorphization and runtime dynamic dispatch. Examples in the standard library include Iterator, Display, Debug, From, all of which are traits and are key vocabulary in library development.

Learning Objectives

  • Define a trait with the trait keyword and implement it for a type with impl Trait for Type.
  • Define a default method to provide common behavior.
  • Implement runtime polymorphism using &dyn Trait / Box<dyn Trait> trait objects.
  • Frequently used traits (Debug, Clone, PartialEq) are automatically implemented with derive.

Key Concepts

1) Defining and Implementing Traits

trait Summary {
    fn title(&self) -> String;            //Signature only
    fn summarize(&self) -> String {        //Basic implementation
        format!("({}) Summary", self.title())
    }
}

impl Summary for Article {
    fn title(&self) -> String { self.headline.clone() }
}

Implementers must provide the methods that only have a signature. Default methods can be left as-is or overridden.

2) Trait Parameters

fn print(item: &impl Summary) { /* ... */ }       // impl Trait
fn print<T: Summary>(item: &T) { /* ... */ }      // Generic + bound
fn print<T>(item: &T) where T: Summary { /* ... */ } // where clause

The three notations have the same meaning. Choose based on readability.

3) Trait Object — Runtime Polymorphism

Used when you want to put multiple types implementing the same trait into a single collection or when the return type needs to be determined dynamically.

fn area(s: &dyn Shape) -> f64 { s.area() }
let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle{...}), Box::new(Square{...})];
  • &dyn Trait — reference-type trait object
  • Box<dyn Trait> — heap-allocated owning trait object

(compared to static dispatch (impl Trait), there is a slight cost when calling because it goes through the virtual function table, but the code becomes more flexible.)

4) Automatic Implementation of derive

Trait Effect
Debug Prints {:?}
Clone Deep copy of .clone()
Copy Automatic bit copy (Clone also needed)
PartialEq Comparison of == / !=
Eq, Hash, Ord Keys for sorted or hash containers
Default Default for T::default()

5) When a Trait is Blocked — orphan rule

A trait you define can be implemented for any type, and any trait can be implemented for a type you define. Both external is not allowed.

Combinations like String with Display (external+external) cannot be implemented directly.

Examples

Example 1 — ex01_summary.rs: Summary trait and default methods

//Example 1 of Chapter 14: Trait Definition, Implementation, and Default Methods
//
//A trait is similar to an "interface" in other languages, but it can have default methods.
//It can provide common behavior without forcing partial implementation.

trait Summary {
    //Methods with empty implementations — must be defined by the implementer
    fn title(&self) -> String;

    //Default methods — this body is used if the implementer does not override
    fn summarize(&self) -> String {
        format!("A summary of ({})", self.title())
    }
}

struct Article {
    headline: String,
    body: String,
}

impl Summary for Article {
    fn title(&self) -> String {
        self.headline.clone()
    }
    //summarize uses the default implementation

    //If you want to see the body as well, you can directly override
    //fn summarize(&self) -> String {
    //    format!("[{}] {} ...", self.headline, &self.body[..self.body.len().min(20)])
    //}
}

struct Tweet {
    user: String,
    text: String,
}

impl Summary for Tweet {
    fn title(&self) -> String {
        format!("@{}", self.user)
    }
    fn summarize(&self) -> String {
        format!("@{}: {}", self.user, self.text)
    }
}

fn main() {
    let a = Article {
        headline: String::from("Rust 1.0 release"),
        body: String::from("Rust has reached a stable version."),
    };
    let t = Tweet {
        user: String::from("rustlang"),
        text: String::from("hello from rustlang"),
    };
    println!("{}", a.summarize());
    println!("{}", t.summarize());
}

Example 2 — ex02_shape.rs: trait object

//Example 14-2: trait object (`dyn Shape`) — Runtime polymorphism
//
//To put different types that implement a trait into the same collection and call the same method,
//use a "trait object." Usually `Box<dyn Trait>` or `&dyn Trait`.

use std::f64::consts::PI;

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &'static str;
}

struct Circle { r: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { PI * self.r * self.r }
    fn name(&self) -> &'static str { "Circle" }
}

impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
    fn name(&self) -> &'static str { "Square" }
}

fn print_area(s: &dyn Shape) {
    println!("{}: area = {:.2}", s.name(), s.area());
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { r: 3.0 }),
        Box::new(Square { side: 4.0 }),
    ];
    for s in &shapes {
        print_area(s.as_ref());
    }
}

Example 3 — ex03_derive.rs: derive macro

//Example 3 of Chapter 14: Automatic Implementation of Common Traits with the derive Macro
//
//Commonly used traits can be automatically implemented with `#[derive(...)]`.

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };

    //Clone — Deep copy
    let p2 = p1.clone();

    //PartialEq — Can use == / != for comparison
    println!("p1 == p2 ? {}", p1 == p2);

    //Debug — Can be printed with {:?} / {:#?}
    println!("p1 = {:?}", p1);
    println!("p1 = {:#?}", p1); //Indentation format
}

Common Mistakes

Q. What is the difference between impl Trait and dyn Trait?

A. impl Trait is monomorphized at compile time, so the call cost is 0, but you can only use one concrete type per function. dyn Trait uses runtime dynamic dispatch, so you can mix multiple types in the same place, but there is a virtual function table lookup cost when calling.

Q. Why doesn't Vec<dyn Trait> work?

A. A trait object is a dynamically sized type whose size cannot be known at compile time, so it cannot go directly into Vec elements. You need to add one level of indirection and use it like Vec<Box<dyn Trait>>.

Q. I only used derive(Clone), but calling clone() doesn't work.

A. Some types of fields inside the structure may not implement Clone. All fields must be Clone for automatic derive to pass.

Q. Can I return Self in a trait?

A. It is possible. However, to make it a trait object (because of object safety rules), you cannot have a method that returns Self. At the introductory level, it is safer to handle it with static dispatch (impl Trait).

Summary

  • The basic pattern is defining trait + implementing impl Trait for Type.
  • Provide common behavior with default methods, and implementations can override when needed.
  • Express runtime polymorphism with &dyn Trait / Box<dyn Trait>.
  • Common traits are automatically implemented with #[derive(...)] — Debug, Clone, PartialEq are frequently used.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/traits/Cargo.tomlTOML
[package]
name = "traits"
version = "0.1.0"
edition = "2021"
default-run = "traits"

[dependencies]
src/traits/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_summary");
    println!("  cargo run --bin ex02_shape");
    println!("  cargo run --bin ex03_derive");
}

Assignment

Problem 1 — Implementing Two Types of Greet Trait

  • File name: homework_01/src/main.rs
  • Core concept: defining a trait + multiple implementations

Requirements

  • Define the trait Greet.
  • fn name(&self) -> &str; (signature only)
  • fn hello(&self) -> String { format!("Hello, {}!", self.name()) } (default method)
  • Define the structs Person { name: String } and Dog { name: String } and implement Greet for both.
  • Person uses the default method as is.
  • Dog overrides hello and returns "{name}: Woof!".
  • In main, print hello() of both instances.

Expected output

Hello, Jisoo!
Choco: Woof!

Hint

  • impl Greet for Person { fn name(&self) -> &str { &self.name } }.
  • Dog also defines fn hello(&self) -> String { format!("{}: Woof!", self.name) }.

Problem 2 — Trait Object Collection

  • File name: homework_02/src/main.rs
  • Core concept: Vec<Box<dyn Trait>>

Requirements

  • Trait Speak { fn speak(&self) -> String; }.
  • Implement the two structs Cat and Cow so that they return "Meow" and "Moo" respectively.
  • Put the two instances in Vec<Box<dyn Speak>> and print them using a for loop.

Expected output

Meow
Moo

Hint

  • let animals: Vec<Box<dyn Speak>> = vec![Box::new(Cat), Box::new(Cow)];
  • for a in &animals { println!("{}", a.speak()); }.

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- The basic methods of a trait are automatically used if the implementation leaves them as is.
//- Other implementations can freely override the same methods.

trait Greet {
    fn name(&self) -> &str;
    fn hello(&self) -> String {
        format!("Hello, {}!", self.name())
    }
}

struct Person { name: String }
struct Dog { name: String }

impl Greet for Person {
    fn name(&self) -> &str { &self.name }
    //hello uses the default implementation
}

impl Greet for Dog {
    fn name(&self) -> &str { &self.name }
    fn hello(&self) -> String { format!("{}: Woof!", self.name) }
}

fn main() {
    let p = Person { name: String::from("Jisoo") };
    let d = Dog { name: String::from("Choco") };

    println!("{}", p.hello());
    println!("{}", d.hello());
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- To put a dynamic sized type (dyn Trait) into a Vec, one level of indirection is needed → Box.
//- Instances with different concrete types can also be handled with the same interface.

trait Speak {
    fn speak(&self) -> String;
}

struct Cat;
struct Cow;

impl Speak for Cat {
    fn speak(&self) -> String { String::from("Meow") }
}

impl Speak for Cow {
    fn speak(&self) -> String { String::from("Moo") }
}

fn main() {
    let animals: Vec<Box<dyn Speak>> = vec![Box::new(Cat), Box::new(Cow)];
    for a in &animals {
        println!("{}", a.speak());
    }
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now