DEV Community

Cover image for Trait in Rust Explained: From Basics to Advanced Usage
Leapcell
Leapcell

Posted on

Trait in Rust Explained: From Basics to Advanced Usage

Cover

What is a Trait?

In Rust, a trait is a way to define shared behavior. It allows us to specify methods that a type must implement, thereby enabling polymorphism and interface abstraction.

Here is a simple example that defines a trait named Printable, which includes a method called print:

trait Printable {
    fn print(&self);
}
Enter fullscreen mode Exit fullscreen mode

Defining and Implementing Traits

To define a trait, we use the trait keyword, followed by the trait name and a pair of curly brackets. Inside the curly brackets, we define the methods that the trait includes.

To implement a trait, we use the impl keyword, followed by the trait name, the for keyword, and the type for which we are implementing the trait. Inside the curly brackets, we must provide implementations for all the methods defined in the trait.

Below is an example showing how to implement the previously defined Printable trait for the i32 type:

impl Printable for i32 {
    fn print(&self) {
        println!("{}", self);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we implemented the Printable trait for the i32 type and provided a simple implementation of the print method.

Trait Inheritance and Composition

Rust allows us to extend existing traits through inheritance and composition. Inheritance enables us to reuse methods defined in a parent trait within a new trait, while composition allows us to use multiple different traits in a new trait.

Here is an example demonstrating how to use inheritance to extend the Printable trait:

trait PrintableWithLabel: Printable {
    fn print_with_label(&self, label: &str) {
        print!("{}: ", label);
        self.print();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a new trait called PrintableWithLabel, which inherits from the Printable trait. This means that any type implementing PrintableWithLabel must also implement Printable. Additionally, we provide a new method, print_with_label, which prints a label before printing the value.

Here is another example demonstrating how to use composition to define a new trait:

trait DisplayAndDebug: Display + Debug {}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a new trait DisplayAndDebug, which consists of two traits from the standard library: Display and Debug. This means that any type implementing DisplayAndDebug must also implement both Display and Debug.

Traits as Parameters and Return Values

Rust allows us to use traits as parameters and return values in function signatures, making our code more generic and flexible.

Here is an example showing how to use the PrintableWithLabel trait as a function parameter:

fn print_twice<T: PrintableWithLabel>(value: T) {
    value.print_with_label("First");
    value.print_with_label("Second");
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a function named print_twice that takes a generic parameter T. The parameter must implement the PrintableWithLabel trait. Inside the function body, we call the print_with_label method on the parameter.

Here is an example showing how to use a trait as a function return value:

fn get_printable() -> impl Printable {
    42
}
Enter fullscreen mode Exit fullscreen mode

However, fn get_printable() -> impl Printable { 42 } is incorrect because 42 is an integer and does not implement the Printable trait.

The correct approach is to return a type that implements the Printable trait. For example, if we implement Printable for the i32 type, we can write:

impl Printable for i32 {
    fn print(&self) {
        println!("{}", self);
    }
}

fn get_printable() -> impl Printable {
    42
}
Enter fullscreen mode Exit fullscreen mode

In this example, we implement the Printable trait for the i32 type and provide a simple implementation of the print method. Then, in the get_printable function, we return an i32 value 42. Since the i32 type implements the Printable trait, this code is correct.

Trait Objects and Static Dispatch

In Rust, we can achieve polymorphism in two ways: static dispatch and dynamic dispatch.

  • Static dispatch is achieved using generics. When we use generic parameters, the compiler generates separate code for each possible type. This allows the function calls to be determined at compile time.
  • Dynamic dispatch is achieved using trait objects. When we use trait objects, the compiler generates a general-purpose code that can handle any type implementing the trait. This allows function calls to be determined at runtime.

Here is an example demonstrating how to use both static dispatch and dynamic dispatch:

fn print_static<T: Printable>(value: T) {
    value.print();
}

fn print_dynamic(value: &dyn Printable) {
    value.print();
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • print_static uses a generic parameter T, which must implement the Printable trait. When this function is called, the compiler generates separate code for each type that is passed to it (static dispatch).
  • print_dynamic uses a trait object (&dyn Printable) as a parameter. This enables dynamic dispatch, allowing the function to process any type implementing the Printable trait.

Associated Types and Generic Constraints

In Rust, we can use associated types and generic constraints to define more complex traits.

Associated Types

Associated types allow us to define a type that is associated with a particular trait. This is useful for defining methods that depend on an associated type.

Here is an example defining a trait named Add using an associated type:

trait Add<RHS = Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • We define a trait called Add.
  • It includes an associated type Output, which represents the return type of the add method.
  • The RHS generic parameter specifies the right-hand side of the addition operation, defaulting to Self.

Generic Constraints

Generic constraints allow us to specify that a generic parameter must satisfy certain conditions (e.g., implement a specific trait).

Here is an example demonstrating how to use generic constraints in a trait named SummableIterator:

use std::iter::Sum;

trait SummableIterator: Iterator
where
    Self::Item: Sum,
{
    fn sum(self) -> Self::Item {
        self.fold(Self::Item::zero(), |acc, x| acc + x)
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • We define a trait SummableIterator that extends the standard Iterator trait.
  • We use a generic constraint (where Self::Item: Sum) to specify that the Item type of the iterator must implement the Sum trait.
  • The sum method calculates the total sum of all elements in the iterator.

Example: Implementing Polymorphism Using Traits

Here is an example demonstrating how to use the PrintableWithLabel trait to achieve polymorphism:

struct Circle {
    radius: f64,
}

impl Printable for Circle {
    fn print(&self) {
        println!("Circle with radius {}", self.radius);
    }
}

impl PrintableWithLabel for Circle {}

struct Square {
    side: f64,
}

impl Printable for Square {
    fn print(&self) {
        println!("Square with side {}", self.side);
    }
}

impl PrintableWithLabel for Square {}

fn main() {
    let shapes: Vec<Box<dyn PrintableWithLabel>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];

    for shape in shapes {
        shape.print_with_label("Shape");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • We define two structs: Circle and Square.
  • Both structs implement the Printable and PrintableWithLabel traits.
  • In the main function, we create a vector shapes that stores trait objects (Box<dyn PrintableWithLabel>).
  • We iterate over the shapes vector and call print_with_label on each shape.

Since both Circle and Square implement PrintableWithLabel, they can be stored as trait objects in a vector. When we call print_with_label, the compiler dynamically determines which method to invoke based on the actual type of the object.

This is how traits enable polymorphism in Rust. I hope this article helps you understand traits better.


We are Leapcell, your top choice for hosting Rust projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ


Read on our blog

Top comments (0)