11.1 Traits in Rust

A trait is Rust’s way of defining a collection of methods that a type must implement. This concept closely resembles interfaces in Java or abstract base classes in C++, though it is a bit more flexible. In C, one might rely on function pointers embedded in structs to achieve a similar effect, but Rust’s trait system provides more compile-time checks and safety guarantees.

Key Concepts

  • Definition: A trait outlines one or more methods that a type must implement.
  • Purpose: Traits enable both code reuse and abstraction by letting functions and data structures operate on any type that implements the required trait.
  • Polymorphism: Traits allow treating different types uniformly, as long as those types implement the same trait. This approach provides polymorphism akin to inheritance in languages like C++—but without a large class hierarchy.

11.1.1 Declaring Traits

Declare a trait using the trait keyword, followed by the trait name and a block containing the method signatures. Traits can include default method implementations, but a type is free to override those defaults:

trait TraitName {
    fn method_name(&self);
    // Additional method signatures...
}

Example:

trait Summary {
    fn summarize(&self) -> String;
}

Any type that implements Summary must provide a summarize method returning a String.

11.1.2 Implementing Traits

Implement a trait for a specific type using impl <Trait> for <Type>:

impl TraitName for TypeName {
    fn method_name(&self) {
        // Method implementation
    }
}

Example

#![allow(unused)]
fn main() {
struct Article {
    title: String,
    content: String,
}

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

The Article struct implements the Summary trait by defining a summarize method.

Implementing Multiple Traits

A single type can implement multiple traits. Each trait is implemented in its own impl block, allowing you to piece together a variety of behaviors in a modular fashion.

11.1.3 Default Implementations

Traits can supply default method bodies. If an implementing type does not provide its own method, the trait’s default behavior will be used:

#![allow(unused)]
fn main() {
trait Greet {
    fn say_hello(&self) {
        println!("Hello!");
    }
}

struct Person {
    name: String,
}

impl Greet for Person {}
}

In this case, Person relies on the default say_hello. To override it:

impl Greet for Person {
    fn say_hello(&self) {
        println!("Hello, {}!", self.name);
    }
}

11.1.4 Trait Bounds

Trait bounds specify that a generic type must implement a certain trait. This ensures the type has the methods or behavior the function needs. For example:

fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

T: Summary tells the compiler that T implements Summary, guaranteeing the presence of a summarize method.

11.1.5 Traits as Parameters

A more concise way to express a trait bound in function parameters uses impl <Trait>:

fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

This is shorthand for fn notify<T: Summary>(item: &T).

11.1.6 Returning Types that Implement Traits

Functions can declare they return a type implementing a trait by using -> impl Trait:

fn create_summary() -> impl Summary {
    Article {
        title: String::from("Generics in Rust"),
        content: String::from("Generics allow for code reuse..."),
    }
}

All return paths in such a function must yield the same concrete type, though they share the trait implementation.

11.1.7 Blanket Implementations

A blanket implementation provides a trait implementation for all types satisfying certain bounds, letting you expand functionality across many types:

use std::fmt::Display;

impl<T: Display> ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}

Here, any type T implementing Display automatically gets an implementation of ToString.