Skip to content

Types, Traits, and Generics

How is a struct different from an enum?

Answer

A struct is a product type: all of its fields exist at the same time. An enum is a sum type: at any moment, exactly one of its variants is active.

This matters because Rust enums are full algebraic data types, not the numbered constants you know from C. Each variant can carry its own data.

An enum takes roughly as much memory as its largest variant, plus a discriminant (and some padding for alignment). The compiler can often skip the discriminant by using a niche, an invalid bit pattern. For example, None in Option<&T> is stored as a null pointer, so Option<&T> is the same size as &T.

rust
enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

fn main() {
    println!("{}", area(&Shape::Rect { w: 2.0, h: 3.0 }));
}

match has to cover every variant. If you add a new variant later, the compiler will point out every match that doesn't handle it.

What is a trait, and how is it different from an interface in Java?

Answer

A trait is a set of methods, associated types, and constants that a type can implement. Traits can have default method implementations and can be generic.

The main differences from Java interfaces:

  • Implementations are separate from the type. In Java, a class has to declare implements when it's defined. In Rust, you can implement your own trait for a type you don't own, even for i32 or Vec<T> (as long as you follow the orphan rule).
  • Static dispatch by default. Generic code with trait bounds is monomorphized, so calls are resolved at compile time. Dynamic dispatch through dyn Trait is opt-in.
  • Traits are more expressive. Associated types, blanket implementations (impl<T: Display> MyTrait for T), and methods that return Self have no direct equivalent in Java.
rust
trait Greet {
    fn name(&self) -> &str;

    fn hello(&self) {
        println!("hi {}", self.name());
    }
}

struct Cat;

impl Greet for Cat {
    fn name(&self) -> &str { "cat" }
}

fn main() {
    Cat.hello();
}

Any type that implements name gets hello for free.

Static vs. dynamic dispatch: pros and cons

Answer

Static dispatch means generics and monomorphization. The compiler generates a separate copy of the function for each concrete type. Calls are direct and can be inlined, so the code is fast, but binaries get bigger and compile times get longer.

Dynamic dispatch means dyn Trait. Methods are called indirectly through a vtable (a table of function pointers). The binary is smaller, and you can mix different types in one collection like Vec<Box<dyn Trait>>. The downside is the cost of the indirect call and the fact that the compiler usually can't inline it.

rust
trait Op { fn run(&self) -> i32; }

struct A;
impl Op for A { fn run(&self) -> i32 { 1 } }

struct B;
impl Op for B { fn run(&self) -> i32 { 2 } }

// One copy is generated for each T
fn sum_static<T: Op>(x: &T, y: &T) -> i32 { x.run() + y.run() }

// One copy for all types; calls go through the vtable
fn sum_dyn(ops: &[Box<dyn Op>]) -> i32 { ops.iter().map(|o| o.run()).sum() }

Rule of thumb: if the types are known at compile time, use generics. For plugins, collections of mixed types, or behavior chosen at runtime, use dyn.

What is dyn compatibility (object safety), and why can't every trait be used as dyn?

Answer

To be used as dyn Trait, a trait must be dyn compatible (this used to be called "object safe"). The main rules:

  • Methods can't have generic type parameters.
  • Methods can't return Self or use Self in their arguments, except as the receiver (&self, &mut self, self: Box<Self>, and so on).
  • Every method must have a receiver. Static functions like fn new() -> Self aren't allowed.
  • Methods can't return impl Trait (which also rules out async fn).
  • The trait can't have associated constants and can't require Self: Sized.

Why. A dyn Trait is a pointer to the data plus a pointer to a vtable, and the vtable has a fixed set of slots, one per method. A generic method would need a separate slot for every possible type parameter, which is impossible. A method that returns Self would return a value whose size isn't known when calling through dyn.

rust
trait Bad { fn make() -> Self; }            // not dyn compatible
trait Good { fn name(&self) -> &str; }      // dyn compatible

fn use_good(x: &dyn Good) { println!("{}", x.name()); }

The workaround: add where Self: Sized to the methods that break the rules. The trait becomes dyn compatible, and those methods simply can't be called through dyn Trait:

rust
trait Shape {
    fn area(&self) -> f64;

    fn new_unit() -> Self where Self: Sized;
}

Another option is to split the trait in two: a dyn-compatible part and an extension trait with the generic methods.

Associated types vs. generic trait parameters: what's the difference?

Answer

A generic parameter on a trait lets one type implement the trait many times, once for each parameter. An associated type is fixed by the implementation: a type can implement the trait only once, so there's exactly one choice.

Iterator uses an associated type Item, because it makes sense for a given iterator to yield just one type of item. From<T> uses a generic parameter, because one type can be created from many different types.

rust
trait Counter {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct UpTo {
    i: u32,
    end: u32,
}

impl Counter for UpTo {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.i < self.end {
            self.i += 1;
            Some(self.i - 1)
        } else {
            None
        }
    }
}

If Item were a generic parameter, the compiler couldn't infer it from the iterator type alone, and you'd have to spell it out all over the place.

What is the orphan rule, and why does it exist?

Answer

The orphan rule says you can implement a trait for a type only if the trait or the type (or both) is defined in your crate. (The full rule has a few extra details for generic types, but this is the core idea.)

Without it, two unrelated libraries could each implement the same trait for the same type, and a program that uses both wouldn't know which implementation to pick. The orphan rule is what keeps the trait system coherent.

The usual workaround is a newtype: wrap the foreign type in your own struct.

rust
use std::fmt;

struct Wrap(Vec<i32>);

impl fmt::Display for Wrap {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

Both Display and Vec come from the standard library, but Wrap is local, so this implementation is allowed.

What is the newtype pattern, and what is it used for?

Answer

A newtype is a tuple struct with a single field that wraps another type. It's used to:

  • work around the orphan rule,
  • give domain values their own types, so you can't mix them up,
  • hide the internal representation behind your own API,
  • provide a different trait implementation for an existing type.

A newtype has no runtime cost. In practice it has the same layout as the inner type, and with #[repr(transparent)] that's guaranteed (which matters for FFI).

rust
struct Meters(f64);
struct Seconds(f64);

fn speed(d: Meters, t: Seconds) -> f64 { d.0 / t.0 }

fn main() {
    println!("{}", speed(Meters(100.0), Seconds(9.58)));
}

The compiler won't let you pass seconds where meters are expected. It's a cheap, compile-time guard against silly bugs.

What is derive, and which traits are commonly derived?

Answer

#[derive(...)] is an attribute that runs a macro to generate a trait implementation for you.

The standard library traits you can derive are Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, and Ord. Deriving works when every field (or every variant's data) already implements the trait. For generic types, derive also adds a bound on each type parameter, e.g. impl<T: Clone> Clone for Wrapper<T>.

Libraries add their own derive macros too, like Serialize and Deserialize from serde, or Error from thiserror.

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

fn main() {
    let p = Point::default();
    println!("{:?}", p.clone());
}

Derive saves you from writing boilerplate and keeps implementations correct during refactoring: when you add a field, every derived implementation picks it up automatically.

What does impl Trait mean in return position and in argument position?

Answer

In return position, impl Trait means "one specific type that implements the trait, without naming it." It's most often used to return iterators and closures, whose real types are long or can't be written at all. The function chooses the type, and it's the same type on every call.

In argument position, impl Trait is a shorthand for a generic parameter: fn log(item: impl Display) is roughly the same as fn log<T: Display>(item: T).

rust
fn iter_evens(v: &[i32]) -> impl Iterator<Item = &i32> {
    v.iter().filter(|x| *x % 2 == 0)
}

fn log(item: impl std::fmt::Display) {
    println!("{}", item);
}

With a returned impl Trait, keep in mind that the hidden type can capture lifetimes from the arguments. Since the 2024 edition, it captures all lifetimes in scope by default. If you need to capture less, list exactly what's captured with use<...>, for example -> impl Iterator<Item = i32> + use<>.

What should you know about using dyn Trait in struct fields?

Answer

dyn Trait is an unsized type, so you can't store it in a field directly. You need some kind of pointer: usually Box<dyn Trait>, sometimes &dyn Trait, Rc<dyn Trait>, or Arc<dyn Trait>.

The choice comes down to ownership. If the struct is long-lived and should own the object, use Box (or Arc if it's shared). If the struct only uses someone else's object, use &dyn Trait with a lifetime.

rust
trait Logger {
    fn log(&self, msg: &str);
}

struct App {
    logger: Box<dyn Logger + Send + Sync>,
}

struct Console;

impl Logger for Console {
    fn log(&self, m: &str) { println!("{}", m); }
}

fn main() {
    let app = App { logger: Box::new(Console) };
    app.logger.log("ok");
}

This is a typical dependency injection pattern. The Send + Sync bounds let App be shared between threads.

Also note that Box<dyn Trait> implicitly means Box<dyn Trait + 'static>, so the object can't hold short-lived references.

What are Sized and ?Sized?

Answer

Sized is a marker trait for types whose size is known at compile time: i32, String, &T, Box<T>, and so on. Types that aren't Sized are called dynamically sized types (DSTs): str, [T], and dyn Trait. You can't hold them by value, only behind a pointer like &str or Box<[T]>.

Every generic parameter has an implicit T: Sized bound. If you want to accept DSTs, relax it with T: ?Sized.

rust
fn print_ref<T: ?Sized + std::fmt::Debug>(x: &T) {
    println!("{:?}", x);
}

fn main() {
    print_ref::<str>("hello");
    print_ref::<[i32]>(&[1, 2, 3]);
}

Without ?Sized, this wouldn't compile for str or slices.

What is a where clause, and when is it better than inline bounds?

Answer

A where clause lets you list bounds separately from the generic parameters. This helps when there are many bounds, or when they're complex, for example bounds on associated types like I::Item: Display, which you can't write inside the angle brackets at all.

In large codebases, where also puts each bound on its own line, which is easier to read and produces cleaner diffs.

rust
use std::fmt::Display;

fn print_all<I>(iter: I)
where
    I: IntoIterator,
    I::Item: Display,
{
    for x in iter {
        println!("{}", x);
    }
}

fn main() {
    print_all(vec!["a", "b"]);
}

Once signatures get complicated, where is almost always easier to read than a long line of angle brackets.

What is coherence, and why are overlapping implementations forbidden?

Answer

Coherence means that for any trait and type, there is at most one implementation in the whole program. Without it, the same call could resolve to different implementations in different places, and the program's behavior would be unpredictable.

Rust enforces coherence with two rules: the orphan rule, and a ban on overlapping implementations. The compiler rejects any pair of impls that could apply to the same type, even if no such type exists today.

rust
trait Foo {}

impl<T> Foo for T {}
impl Foo for i32 {} // E0119: conflicting implementations of trait `Foo` for type `i32`

Specialization, which would allow a more specific impl to override a general one, is exactly what this rule forbids. That's why it's still unstable: making it sound turned out to be very hard.

On stable Rust, the usual workarounds are to drop the blanket impl and implement the trait for each type separately (often with a macro), or to use a newtype.

What is a supertrait, and when do you need one?

Answer

A supertrait is a requirement that any type implementing a trait must also implement another trait. It's written as trait Child: Parent.

Inside the child trait, you can use the parent trait's methods on self. Parent methods can also be called on &dyn Child, and since Rust 1.86 you can convert &dyn Child into &dyn Parent (trait upcasting).

rust
use std::fmt::Display;

trait Pretty: Display {
    fn pretty(&self) -> String {
        format!("=> {}", self)
    }
}

impl Pretty for i32 {}

fn main() {
    println!("{}", 7i32.pretty());
}

pretty can use self with format! because every Pretty type is guaranteed to implement Display.

What is a blanket implementation, and where does the standard library use them?

Answer

A blanket implementation implements a trait for every type that meets some bound.

The best-known examples from the standard library:

  • impl<T: Display + ?Sized> ToString for T gives to_string() to everything that implements Display.
  • impl<T, U> Into<U> for T where U: From<T> gives you Into for free as soon as you implement From.

This is a powerful tool, but because of the ban on overlapping impls it has a cost: once a blanket impl exists, nobody can write an impl that overlaps with it. For example, you can't implement ToString directly for a type that implements Display. For the same reason, adding a new blanket impl to a published library is a breaking change.

rust
struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 1.8 + 32.0)
    }
}

fn main() {
    let f: Fahrenheit = Celsius(100.0).into(); // `into` comes from the blanket impl
    println!("{}", f.0);
}

Only From is written by hand here. Into is provided by the standard library's blanket impl.