11.1 Traits: Defining Shared Behavior

A trait in Rust defines a set of methods that a type must implement to conform to a certain interface or contract. Traits are central to Rust’s abstraction capabilities, enabling polymorphism and code sharing. For C programmers, think of them as a more formalized and compile-time-checked version of using function pointers within structs to achieve polymorphism.

Key Concepts

  • Definition: A trait block specifies method signatures that constitute a shared behavior. Optionally, it can also provide default implementations for some methods.
  • Implementation: Types opt into a trait’s behavior using an impl Trait for Type block, providing concrete implementations for the required methods, or relying on defaults if available.
  • Abstraction: Functions and data structures can operate on any type that implements a specific trait, using trait bounds.
  • Polymorphism: Traits allow different types to be treated uniformly based on shared capabilities, similar to how interfaces or abstract classes work, but without inheritance hierarchies.

11.1.1 Declaring and Implementing Traits

A trait is declared with the trait keyword, followed by its name and a block containing method signatures. These signatures define the methods that any type implementing the trait must provide.

Traits can also provide default implementations for methods, which an implementing type can use or overwrite by providing its own version.

Many trait methods take a special first parameter representing the instance the method is called on: self, &self, or &mut self. Note that &self is shorthand for self: &Self, where Self is a type alias for the type implementing the trait (e.g., Article or Tweet in the examples below).

#![allow(unused)]
fn main() {
trait Summary {
    // Method signature: requires implementing types to provide this method.
    fn summarize(&self) -> String; // Takes an immutable reference to the instance

    // A method with a default implementation. Optional for implementors.
    fn description(&self) -> String {
        String::from("(No description)") // Default implementation
    }
}
}

To implement this trait for a specific type, such as a struct, use an impl block. Within this block, you provide the concrete implementations for the methods defined in the trait signature. If the trait provides default implementations, you can choose to override them or use the defaults by simply not providing an implementation for that specific method.

#![allow(unused)]
fn main() {
trait Summary {
    fn summarize(&self) -> String;
    fn description(&self) -> String {
        String::from("(No description)")
    }
}
struct Article {
    title: String,
    content: String,
}

// Implement the Summary trait for the Article struct
impl Summary for Article {
    fn summarize(&self) -> String {
        // Provide a concrete implementation for summarize
        if self.content.len() > 50 {
            format!("{}...", &self.content[..50])
        } else {
            self.content.clone()
        }
    }
    // We don't provide `description`, so the default implementation from the
    // trait definition is used for Article instances.
}

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

// Implement the Summary trait for the Tweet struct
impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.text)
    }

    // Override the default implementation for description
    fn description(&self) -> String {
        format!("Tweet by @{}", self.username)
    }
}
}

As shown above, Article uses the default description, while Tweet overrides it. A single type can implement multiple different traits, allowing types to compose behaviors in a modular way. Each trait implementation typically resides in its own impl block.

11.1.2 Using Traits as Parameters (Trait Bounds)

You can write functions that accept any type implementing a specific trait using trait bounds. This allows functions to operate on data generically, based on capabilities rather than concrete types. This is commonly done using generic type parameters (<T: Trait>) or the impl Trait syntax in argument position.

trait Summary {
    fn summarize(&self) -> String;
    fn description(&self) -> String {
        String::from("(No description)")
    }
}

struct Article {
    title: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        if self.content.len() > 50 {
            format!("{}...", &self.content[..50])
        } else {
            self.content.clone()
        }
    }
}

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

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

    fn description(&self) -> String {
        format!("Tweet by @{}", self.username)
    }
}
// Using generic type parameter 'T' with a trait bound 'Summary'
fn print_summary<T: Summary>(item: &T) {
    println!("Summary: {}", item.summarize());
    println!("Description: {}", item.description());
}

// Using 'impl Trait' syntax (often more concise for simple cases)
fn notify(item: &impl Summary) {
    println!("Notification! {}", item.summarize());
}

fn main() {
    let article = Article {
        title: String::from("Rust Traits"),
        content: String::from("Traits define shared behavior across different ..."),
    };
    let tweet = Tweet {
        username: String::from("rustlang"),
        text: String::from("Check out the new release!"),
    };

    print_summary(&article); // Works with Article
    notify(&tweet);         // Works with Tweet
}

Both print_summary and notify can operate on any type that implements Summary, demonstrating polymorphism. Under the hood, Rust typically uses static dispatch (monomorphization) for generic functions like these, meaning specialized code is generated for each concrete type (Article and Tweet), ensuring high performance.

11.1.3 Returning Types that Implement Traits

Just as functions can accept arguments of types implementing a trait, they can also return values specified only by the trait they implement. This is done using impl Trait in the return type position. This technique allows a function to hide the specific concrete type it’s returning, providing encapsulation.

trait Summary {
    fn summarize(&self) -> String;
}
struct Article {
    title: String,
    content: String,
}
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("Article: {}...", &self.title) // Simplified for brevity
    }
}

// This function returns *some* type that implements Summary.
// The caller knows it implements Summary, but not the concrete type (Article).
fn create_summary_item() -> impl Summary {
    Article {
        title: String::from("Return Types"),
        content: String::from("Using impl Trait in return position..."),
    }
    // Note: All possible return paths within the function must ultimately
    // return the *same* concrete type (here, always Article).
}

fn main() {
    let summary_item = create_summary_item();
    println!("Created Item: {}", summary_item.summarize());
}

This approach is useful for simplifying function signatures when the concrete return type is complex or an implementation detail the caller doesn’t need to know.

11.1.4 Blanket Implementations

Rust allows implementing a trait for all types that satisfy another trait bound. This powerful feature is called a blanket implementation. It enables extending functionality across a wide range of types concisely.

A prominent example involves the standard library traits ToString and Display. The Display trait is intended for formatting types in a user-facing, human-readable way; it’s the trait used by the {} format specifier in println! and related macros. The standard library provides a blanket implementation of ToString for any type that implements Display.

// From the standard library (simplified):
use std::fmt::Display;

// Implement 'ToString' for any type 'T' that already implements 'Display'.
impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        // This implementation leverages the existing Display implementation
        // to convert the type to a String.
        format!("{}", self)
    }
}

Because of this blanket implementation, any type that implements Display (like numbers, strings, and many standard library types, or your own types if you implement Display for them) automatically gets a to_string method for free, which provides its user-facing string representation.