Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Traits

Core Interview Questions

1. What is a trait in Rust?

A trait defines a set of methods that a type must implement. It's Rust's way of specifying shared behavior across different types. Think of it as a contract—any type that implements the trait promises to provide the methods defined in that trait.

2. What's the difference between static dispatch and dynamic dispatch?

Static dispatch means the compiler knows exactly which method to call at compile time. This happens with generics and trait bounds—the compiler generates specialized code for each concrete type.

Dynamic dispatch means the method to call is determined at runtime through a vtable (virtual table). This happens with trait objects (dyn Trait). You pay a small performance cost for indirection, but you get flexibility—you can store different types that implement the same trait in a single collection.

3. What is the orphan rule and why does it exist?

The orphan rule says you can implement a trait for a type if either the trait or the type is defined in your crate. You can't implement a foreign trait on a foreign type.

This exists to prevent coherence conflicts. If two different crates both implemented Display for Vec<i32>, the compiler wouldn't know which implementation to use. The orphan rule ensures there's always exactly one implementation for any given trait-type combination.

4. What's the difference between impl Trait and dyn Trait?

impl Trait is used in function signatures and means "some concrete type that implements this trait." The actual type is chosen by the function implementation (for return position) or the caller (for argument position). The compiler monomorphizes this—generating separate code for each concrete type.

dyn Trait is a trait object—a dynamically-sized type that erases the concrete type. You use it when you need runtime polymorphism, like storing different implementations in a Vec<dyn Trait>. The method calls go through a vtable, so there's a small runtime cost.

5. How does finding the minimal common interface relate to dependency inversion?

Finding the minimal common interface is dependency inversion in practice. The Dependency Inversion Principle states: "Depend on abstractions, not on concretions."

When you find the minimal common interface and encode it as a trait, you're:

  1. Creating the abstraction - The trait becomes the abstraction layer that both high-level and low-level code depend on
  2. Inverting dependencies - Instead of ServiceA depending directly on ServiceB, both depend on trait ManagedService
  3. Loosening coupling - Concrete implementations can change without breaking callers, as long as they implement the trait

The "tight coupling" through traits means the abstraction closely matches actual needs—so it's a good abstraction that prevents both over-abstraction and under-abstraction.

Code Snippets

Basic Trait Definition and Implementation

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

struct ConsoleLogger;

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

fn main() {
    let logger = ConsoleLogger;
    logger.log("Hello from ConsoleLogger");
}

Trait Bounds (Static Dispatch)

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

struct ConsoleLogger;
struct FileLogger;

impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("[CONSOLE] {}", message);
    }
}

impl Logger for FileLogger {
    fn log(&self, message: &str) {
        println!("[FILE] {}", message);
    }
}

fn send_notification<T: Logger>(logger: &T, msg: &str) {
    logger.log(msg);
}

fn send_notification_where<T>(logger: &T, msg: &str)
where
    T: Logger,
{
    logger.log(msg);
}

fn main() {
    let console = ConsoleLogger;
    let file = FileLogger;

    send_notification(&console, "System started");
    send_notification_where(&file, "File initialized");
}

Trait Objects (Dynamic Dispatch)

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

struct ConsoleLogger;
struct FileLogger;

impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("[CONSOLE] {}", message);
    }
}

impl Logger for FileLogger {
    fn log(&self, message: &str) {
        println!("[FILE] {}", message);
    }
}

fn process_loggers(loggers: &[Box<dyn Logger>]) {
    for logger in loggers {
        logger.log("processing");
    }
}

fn main() {
    let loggers: Vec<Box<dyn Logger>> = vec![
        Box::new(ConsoleLogger),
        Box::new(FileLogger),
    ];

    process_loggers(&loggers);
}

impl Trait in Return Position

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

struct ConsoleLogger;

impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("[CONSOLE] {}", message);
    }
}

fn get_logger() -> impl Logger {
    ConsoleLogger
}

fn main() {
    let logger = get_logger();
    logger.log("Hello from impl Trait");
}

Associated Types vs Generics

// With associated types (standard library approach)
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    current: usize,
    max: usize,
}

impl Iterator for Counter {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let val = self.current;
            self.current += 1;
            Some(val)
        } else {
            None
        }
    }
}

// With generics (less common, more verbose)
trait IteratorGeneric<T> {
    fn next(&mut self) -> Option<T>;
}

struct GenericCounter {
    current: usize,
    max: usize,
}

impl IteratorGeneric<usize> for GenericCounter {
    fn next(&mut self) -> Option<usize> {
        if self.current < self.max {
            let val = self.current;
            self.current += 1;
            Some(val)
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter { current: 0, max: 3 };
    while let Some(val) = counter.next() {
        println!("Got: {}", val);
    }

    let mut gen_counter = GenericCounter { current: 0, max: 3 };
    while let Some(val) = gen_counter.next() {
        println!("Generic got: {}", val);
    }
}