DEV Community

Cover image for [Rust Guide] 10.3. Trait Pt.1 - Trait Definitions, Bounds, and Implementation
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 10.3. Trait Pt.1 - Trait Definitions, Bounds, and Implementation

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

10.3.1 What Is a Trait

Trait means feature or characteristic. Traits are used to describe to the Rust compiler what capabilities a type has and which behaviors it can share with other types. Traits define shared behavior in an abstract way.

There is also the concept of trait bounds, which can constrain a generic type parameter to a type that implements a specific behavior. In other words, it requires the generic type parameter to implement certain traits.

Traits in Rust are somewhat similar to interfaces in other languages, but there are still differences.

10.3.2 Defining a Trait

The behavior of a type is made up of the methods that the type itself can call. Sometimes different types have the same methods, and in that case we say those types share the same behavior. Traits provide a way to group methods together, thereby defining the behavior required to achieve a certain purpose.

  • Use the trait keyword to define a trait. Inside the trait definition, there are only method signatures, no concrete implementations
  • A trait can have multiple methods, and each method is written on its own line and ends with ;
  • The type implementing that trait must provide concrete method implementations, which means method bodies are required

For example:

pub trait Summary {
    fn summarize(&self) -> String;
}
Enter fullscreen mode Exit fullscreen mode

Adding pub before trait makes it public. The trait is named Summary, and it contains a method signature called summarize. Aside from &self, it has no other parameters, the return type is String, and the signature ends with ;. There is no method body, so there is no concrete implementation. Of course, a trait can contain many method signatures:

pub trait Summary {
    fn summarize(&self) -> String;
    fn summarize1(&self) -> String;
    fn summarize2(&self) -> String;
    //......
}
Enter fullscreen mode Exit fullscreen mode

10.3.3 Implementing a Trait for a Type

Implementing a trait for a type is very similar to implementing methods for a type, but there are also differences.

The syntax for implementing methods for a type is to follow the impl keyword with the type:

impl Yyyy {....}
Enter fullscreen mode Exit fullscreen mode

Implementing a trait for a type looks like this:

impl Xxxx for Yyyy {....}
Enter fullscreen mode Exit fullscreen mode
  • Xxxx refers to the trait name
  • Yyyy refers to the type name
  • Inside the braces, you need to write the concrete implementations for the trait’s method signatures

For example (lib.rs):

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

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

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

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The struct NewsArticle represents a news article. It has four fields: headline for the title, location for the location, author for the author, and content for the content
  • The struct Tweet represents a tweet on X (formerly Twitter). It has four fields: username, content, reply, and retweet

These two struct types are certainly different, and most of their fields are different too. But they can both have the same behavior—providing a Summary—so Summary is implemented separately for both types.

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}
Enter fullscreen mode Exit fullscreen mode

This block implements the trait for NewsArticle. Because the trait definition includes the summarize method signature, a concrete implementation must be written here: use the format! macro to combine self.headline, self.author, and self.location into a string and return it.

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Enter fullscreen mode Exit fullscreen mode

This block implements the trait for Tweet as well, again providing the concrete implementation of summarize: use the format! macro to combine self.username and self.content into a string and return it.

Now let’s move to main.rs and look at how the instances are called:

use RustStudy::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}
Enter fullscreen mode Exit fullscreen mode

Remember that our code is written in lib.rs, before using something in main.rs, you need to bring it into scope first. The syntax is:

use your_package_name::...::the_module_you_need;
Enter fullscreen mode Exit fullscreen mode

Your package name is the project name in Cargo.toml; just copy it from there.

Summary is imported because the summarize method under the Summary trait is used. Tweet is imported because the Tweet struct is used.

Look at the output:

1 new tweet: horse_ebooks: of course, as you probably already know, people
Enter fullscreen mode Exit fullscreen mode

10.3.4 Trait Constraints

The prerequisites for implementing a trait for a type are:

  • The type itself (for example Tweet) or the trait itself (for example letting Vector implement a local Summary) must be defined in the local crate
  • You cannot implement an external trait for an external type. For example, in a local crate implementing the standard library’s Display trait for the standard library’s Vector This restriction is part of the language’s coherence rules. More specifically, it is the orphan rule, so named because the parent type is not defined in the current crate. This rule ensures that other people’s code cannot arbitrarily break your code, and vice versa. Without this rule, two crates could implement the same trait for the same type, and Rust would not know which implementation to use.

10.3.5 Default Implementations

Sometimes it is very useful to provide default behavior for some or all methods in a trait. This lets us avoid writing custom behavior for every single type implementation. We can still implement trait methods for specific types.

When implementing a trait for certain types, we can choose whether to keep or override each method’s default implementation.

The previous version was:

pub trait Summary {
    fn summarize(&self) -> String;
}
Enter fullscreen mode Exit fullscreen mode

The previous version only wrote the method signature and did not provide an implementation, but in fact a default implementation can be added:

Default implementation:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}
Enter fullscreen mode Exit fullscreen mode

The default implementation here simply returns the string "(Read more...)".

Because this method already has a default implementation in the trait, a concrete type can use that default implementation directly instead of providing its own.

Using NewsArticle as an example, it originally had its own implementation (also called an override of the default implementation):

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}
Enter fullscreen mode Exit fullscreen mode

If you delete this concrete implementation, NewsArticle will use the default implementation:

impl Summary for NewsArticle {}
Enter fullscreen mode Exit fullscreen mode

There is one more thing to know: a method with a default implementation can call other methods in the trait, even if those methods do not have default implementations:

pub trait Summary {
    fn summarize_author(&self) -> String;
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
Enter fullscreen mode Exit fullscreen mode

The default implementation of summarize calls summarize_author, even though summarize_author is only a signature and has no concrete implementation. But if you want to implement summarize for a type, you first need to implement summarize_author:

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
}
Enter fullscreen mode Exit fullscreen mode

PS: Since NewsArticle uses the default implementation of summarize, there is no need to write a default implementation of summarize here.

One thing to note about this style: you cannot call the default implementation from within an overridden method implementation.

Top comments (0)