Skip to content
Defining Traits
step 1/7

Reading — step 1 of 7

Learn

~3 min readTraits and Generics

Defining Traits

A trait is a contract: any type that implements it provides these methods. Traits are Rust's interfaces (think Java interfaces or Haskell type classes), and they power both compile-time generics and runtime polymorphism.

Defining and implementing

trait Greet {
    fn hello(&self) -> String;          // required — no body

    fn shout(&self) -> String {         // default — overriding is optional
        self.hello().to_uppercase()
    }
}

struct English;
impl Greet for English {
    fn hello(&self) -> String { String::from("hi") }
}

struct Klingon;
impl Greet for Klingon {
    fn hello(&self) -> String { String::from("nuqneH") }
    fn shout(&self) -> String { String::from("NUQNEH!") }   // override
}

Skip a required method and the compiler names exactly what's missing:

error[E0046]: not all trait items implemented, missing: `hello`

And the impl's signatures must match the trait's exactly — fn hello(self) where the trait says fn hello(&self) is error[E0053]: method has an incompatible type for trait.

Using traits: generics (static dispatch)

fn announce<T: Greet>(thing: T) {
    println!("{}", thing.hello());
}

fn complex<T>(x: T)
where
    T: Greet + std::fmt::Debug,   // multiple bounds read better in `where`
{
    println!("{:?} says {}", x, x.hello());
}

T: Greet is a bound: only types implementing Greet may be passed. The compiler then monomorphizes — it generates a separate, fully-typed copy of announce for each concrete type used. Calls are direct and inlinable, zero runtime overhead; the cost is more generated code.

Using traits: trait objects (dynamic dispatch)

Generics fix the type at compile time. When the concrete type is only known at runtime — "circle or square, depending on input" — you need a trait object:

fn announce_all(things: Vec<Box<dyn Greet>>) {
    for t in things {
        println!("{}", t.hello());   // dispatched through a vtable
    }
}

dyn Greet is unsized (an English and a Klingon can have different sizes), so it must live behind a pointer: Box<dyn Greet>, &dyn Greet. The trap:

fn make() -> dyn Greet { /* ... */ }   // no Box
error[E0277]: the size for values of type `dyn Greet` cannot be known at compilation time

Method calls on a trait object go through a vtable — a small runtime cost that buys you one variable holding different concrete types. That's exactly what your exercise's main does: it builds Box::new(Circle {...}) or Box::new(Square {...}) and stores either in a single Box<dyn Shape>.

Your exercise

Implement Shape for both structs:

  • Circle { radius: f64 } — area is 3.14 * self.radius * self.radius (the description says use 3.14 for π, literally)
  • Square { side: f64 } — area is self.side * self.side

main is already written: it reads the kind and the dimension, builds the right shape as a Box<dyn Shape>, and prints s.area() with {:.2}.

TEST 2 feeds circle then 10 and expects exactly:

314.00

Mistakes the grader catches:

  • Using std::f64::consts::PI — more "correct" mathematically, but it prints 314.16 and fails TEST 2, which expects 314.00. The grader wants the literal 3.14.
  • Wrong method signature — anything but fn area(&self) -> f64 inside impl Shape for ... breaks the trait match (E0053) or leaves the trait unimplemented (E0046), and the Box<dyn Shape> in main won't type-check.
  • Touching the {:.2}println!("{}", ...) prints 25 where TEST 1 expects 25.00. Leave main alone; you only need to add the two impl blocks.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…