Skip to main content

Command Palette

Search for a command to run...

Traits

The Chapter 11

Published
6 min readView as Markdown
T
Dev cum DevRel trying to figure out things.

In Chapter 10: Generics, we learned how to write code that can work with different types without having to duplicate the code for each type. Now, let's explore Traits, which define shared behavior that different types can implement.

Imagine you have several different types of vehicles: cars, trucks, and motorcycles. Each of these vehicles can move. Traits let you define what it means to "move" so that you can write code that works with any vehicle that can move, regardless of whether it's a car, truck, or motorcycle.

Central Use Case: You want to write a function that displays information about any item that can be "summarized." This could be a news article, a user profile, or a product description. Traits let you define what it means to be "summarizable" so that you can write one function that works for all these different types.

Key Concepts

Let's break down the key things to understand about traits:

  1. Trait Definition: A trait is a collection of method signatures that define a set of behaviors. Think of it as a contract: if a type implements a trait, it promises to provide implementations for all the methods defined in the trait.

  2. Implementing a Trait: You implement a trait for a specific type using the impl keyword. This tells Rust that the type will provide concrete implementations for the trait's methods.

  3. Trait Bounds: You can use trait bounds to specify that a generic type must implement a particular trait. This allows you to write generic code that only works with types that have certain capabilities.

  4. Default Implementations: Traits can provide default implementations for their methods. This allows types to implement only the methods that are specific to them, while relying on the default implementations for the rest.

Traits in Action: The Summarize Trait

Let's create a Summarize trait:

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

Explanation:

  • pub trait Summarize: This defines a public trait named Summarize. The pub keyword makes the trait accessible from outside the current module.

  • fn summarize(&self) -> String;: This defines a method signature named summarize.

    • &self: This indicates that the method takes a reference to the type implementing the trait.

    • -> String: This indicates that the method returns a String.

Now, let's create a struct and implement the Summarize trait for it:

struct NewsArticle {
    headline: String,
    author: String,
    content: String,
}

impl Summarize for NewsArticle {
    fn summarize(&self) -> String {
        format!("{} by {}", self.headline, self.author)
    }
}

Explanation:

  • struct NewsArticle: This defines a struct named NewsArticle with fields for the headline, author, and content.

  • impl Summarize for NewsArticle: This tells Rust that we're implementing the Summarize trait for the NewsArticle struct.

  • fn summarize(&self) -> String { ... }: This provides a concrete implementation for the summarize method. In this case, it returns a string that combines the headline and author.

Now, let's use the Summarize trait:

fn main() {
    let article = NewsArticle {
        headline: String::from("Rust Traits are Awesome!"),
        author: String::from("Jane Doe"),
        content: String::from("This article explains Rust traits..."),
    };

    println!("Summary: {}", article.summarize());
}

Output:

Summary: Rust Traits are Awesome! by Jane Doe

Explanation:

  • We create an instance of the NewsArticle struct.

  • We call the summarize method on the instance. Because NewsArticle implements the Summarize trait, we can call the method just like any other method on the struct.

Traits in Action: Default Implementations

Let's add a default implementation to the Summarize trait:

pub trait Summarize {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

Explanation:

  • We add a default implementation for the summarize method. This implementation simply returns the string "(Read more...)".

Now, let's create a struct and implement the Summarize trait for it, but without providing a custom implementation for the summarize method:

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

impl Summarize for Tweet {} // No custom summarize implementation!

Explanation:

  • We implement the Summarize trait for the Tweet struct, but we don't provide any code within the impl block. This means that the Tweet struct will use the default implementation of the summarize method.

Now, let's use the Summarize trait:

fn main() {
    let tweet = Tweet {
        username: String::from("johndoe"),
        content: String::from("Rust is great!"),
    };

    println!("Summary: {}", tweet.summarize());
}

Output:

Summary: (Read more...)

Explanation:

  • We call the summarize method on the tweet instance. Because Tweet uses the default implementation of the summarize method, it prints "(Read more...)".

Traits as Parameters

You can use traits as parameters to functions. This lets you write functions that can accept any type that implements the trait.

pub fn display_summary(item: &impl Summarize) {
    println!("Summary: {}", item.summarize());
}

Explanation:

  • pub fn display_summary(item: &impl Summarize): This defines a function named display_summary that takes one argument named item.

    • &impl Summarize: This indicates that the item argument must be a reference to a type that implements the Summarize trait.

Now, let's use this function:

fn main() {
    let article = NewsArticle {
        headline: String::from("Rust Traits are Awesome!"),
        author: String::from("Jane Doe"),
        content: String::from("This article explains Rust traits..."),
    };

    display_summary(&article);

    let tweet = Tweet {
        username: String::from("johndoe"),
        content: String::from("Rust is great!"),
    };

    display_summary(&tweet);
}

Output:

Summary: Rust Traits are Awesome! by Jane Doe
Summary: (Read more...)

Explanation:

  • We call display_summary with both a NewsArticle and a Tweet.

  • The same display_summary function works for both types because both implement the Summarize trait.

Internal Implementation (Simplified)

When you use traits, Rust uses a technique called dynamic dispatch (or "trait objects") in some cases. This means that the compiler doesn't know at compile time which concrete implementation of a trait method will be called. Instead, it figures it out at runtime.

Here's a simplified sequence diagram of how this works:

Explanation:

  1. The Main function calls display_summary with a NewsArticle instance.

  2. DisplaySummary needs to call the summarize method. Since it only knows that the input implements Summarize interface, it doesn't know what the concrete summarize method is. It uses dynamic dispatch to look up the correct implementation at runtime.

  3. Rust finds the summarize implementation for NewsArticle and calls it.

  4. The summarize method returns a string.

  5. The DisplaySummary function prints the summary.

  6. The same process is repeated for the Tweet instance, but this time Rust finds the default implementation of the summarize method.

Important Details (Not Covered Here for Simplicity):

  • Trait Objects: Dynamic dispatch is often implemented using "trait objects," which are essentially pointers to both the data and a table of function pointers (the "vtable") that contains the correct method implementations for that data type.

  • Static Dispatch (Generics): When you use generics with trait bounds (e.g., fn display_summary<T: Summarize>(item: &T)), Rust can use static dispatch. This means that the compiler knows at compile time which implementation of the trait method to call, and it can generate more efficient code.

Conclusion

Traits are a powerful tool in Rust that allows you to define shared behavior that different types can implement. They promote code reuse, abstraction, and polymorphism. Understanding traits is crucial for writing flexible and maintainable Rust code.

In the next chapter, we'll explore Lifetimes, which help the compiler ensure that references are always valid.

Beginner's Guide to Rust

Part 13 of 14

In this series, we'll dive deep into Rust programming

Up next

Lifetimes

The Chapter 12