11.1 Understanding Traits

11.1.1 What Are Traits?

In Rust, a trait is a way to define shared behavior. Traits are similar to interfaces in languages like Java or abstract base classes in C++. They allow you to specify a set of methods that a type must implement to satisfy the trait. Traits enable polymorphism, which is the ability of different types to be treated uniformly based on shared behavior.

Key Points:

  • Definition: A trait defines functionality a type must provide.
  • Purpose: Traits allow for code reuse and abstraction over different types that share common behavior.
  • Polymorphism: Traits enable writing code that can operate on different types as long as they implement the required trait.

Polymorphism is a programming concept that refers to the ability of different types to be treated as if they are of a common type, typically through a shared interface or base class. In Rust, traits enable polymorphism by allowing different types to implement the same trait and be used interchangeably where that trait is expected.

11.1.2 Defining Traits

You define a trait using the trait keyword, followed by the trait name and a block containing method signatures.

Syntax:

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

Example:

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

In this example, the Summary trait requires any implementing type to provide a summarize method that returns a String.

11.1.3 Implementing Traits

To implement a trait for a type, you use the impl keyword along with the trait name for the type.

Syntax:

impl TraitName for TypeName {
    fn method_name(&self) {
        // Implementation...
    }
    // Implement other methods...
}

Example:

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

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

Here, we implement the Summary trait for the Article struct by providing an implementation for the summarize method.

Implementing Multiple Traits:

A type can implement multiple traits, and you can implement traits for any type you define.

11.1.4 Default Implementations

Traits can provide default implementations for methods. This means that implementing types can choose to use the default or provide their own implementation.

Example:

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

struct Person {
    name: String,
}

impl Greet for Person {}
}

In this example, the Person struct implements the Greet trait but doesn't provide its own say_hello method. Therefore, it uses the default implementation.

Overriding Default Implementations:

An implementing type can override the default implementation.

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

11.1.5 Trait Bounds

Trait bounds are used to specify that a generic type parameter must implement a particular trait. This ensures that the generic type provides the necessary behavior.

Example:

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

In this function, T is a generic type that must implement the Summary trait. This allows print_summary to accept any type that implements Summary.

11.1.6 Traits as Parameters

Rust provides a shorthand for specifying trait bounds when using traits as function parameters.

Syntax:

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

Here, &impl Summary is shorthand for &T where T: Summary.

Example:

fn main() {
    let article = Article {
        title: String::from("Rust Traits"),
        content: String::from("Traits are awesome in Rust..."),
    };
    notify(&article);
}

11.1.7 Returning Types that Implement Traits

You can specify that a function returns some type that implements a trait using -> impl Trait.

Example:

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

Note:

  • The concrete type returned must be the same in all cases. You cannot return different types that implement the same trait from a single function using -> impl Trait.
  • This is known as opaque return types.

11.1.8 Blanket Implementations

A blanket implementation is an implementation of a trait for any type that satisfies certain trait bounds. This is a powerful feature in Rust that allows you to implement a trait for all types that implement another trait.

Example:

use std::fmt::Display;

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

In this example, we implement the ToString trait for any type T that implements the Display trait.