Module: Structs and Enums

Traits

Rust Programming: Structs and Enums -> Traits

This document covers Traits in Rust, building upon the concepts of Structs and Enums. Traits are a powerful feature enabling code reuse and polymorphism.

What are Traits?

Traits define shared behavior. Think of them as interfaces in other languages like Java or C#. They specify what a type can do, but not how it does it. A type can implement a trait, providing the concrete implementation for the methods defined in the trait.

Key Concepts:

  • Define Shared Behavior: Traits define a set of methods that types can implement.
  • Abstraction: They abstract away the specific implementation details, focusing on the functionality.
  • Polymorphism: Traits enable you to write code that works with any type that implements a specific trait, regardless of its underlying structure.
  • Code Reuse: Traits promote code reuse by allowing you to define common functionality that can be shared across different types.

Defining a Trait

Traits are defined using the trait keyword. Here's a simple example:

trait Printable {
    fn print(&self); // Method signature - no implementation here!
}

This defines a trait called Printable with a single method print. Notice there's no body for the print method. The trait only declares the method; it doesn't implement it.

Implementing a Trait

To use a trait, a type must implement it. This is done using the impl keyword, followed by the type and the trait name.

struct Point {
    x: i32,
    y: i32,
}

impl Printable for Point {
    fn print(&self) {
        println!("Point: ({}, {})", self.x, self.y);
    }
}

struct Circle {
    radius: f64,
}

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

In this example:

  • We define two structs: Point and Circle.
  • We implement the Printable trait for both Point and Circle.
  • Each implementation provides a specific implementation of the print method tailored to its respective struct.

Using Traits

Now that we've implemented the Printable trait, we can write code that works with any type that implements it.

fn print_item<T: Printable>(item: &T) {
    item.print();
}

fn main() {
    let point = Point { x: 10, y: 20 };
    let circle = Circle { radius: 5.0 };

    print_item(&point);   // Output: Point: (10, 20)
    print_item(&circle);  // Output: Circle: radius = 5
}
  • The print_item function takes a generic type T.
  • The T: Printable constraint ensures that T must implement the Printable trait.
  • Inside print_item, we can call the print method on item because we know it implements Printable.

Default Implementations

Traits can provide default implementations for methods. This allows types to optionally override the default behavior.

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

    // Default implementation
    fn summarize_author(&self) -> String {
        String::from("(Read by: Unknown)")
    }
}

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

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

struct Tweet {
    username: String,
    content: String,
    reply: bool,
    retweets: u32,
}

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

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA"),
        author: String::from("Ice Hockey News"),
        content: String::from("The Pittsburgh Penguins have won the Stanley Cup Championship..."),
    };

    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as a reminder, despite the cost of the box"),
        reply: false,
        retweets: 0,
    };

    println!("News article summary: {}", article.summarize());
    println!("Tweet summary: {}", tweet.summarize());
    println!("News article author: {}", article.summarize_author());
    println!("Tweet author: {}", tweet.summarize_author()); // Uses default implementation
}
  • Summary trait has a summarize method (must be implemented) and summarize_author (has a default implementation).
  • NewsArticle implements summarize to provide a specific summary.
  • Tweet implements summarize to provide a different summary.
  • Tweet doesn't implement summarize_author, so it uses the default implementation.

Trait Bounds

Trait bounds are used to constrain generic types. We've already seen this in the print_item function: T: Printable. This means that T must implement the Printable trait.

You can also use multiple trait bounds:

fn process_data<T: Printable + Debug>(data: &T) {
    data.print();
    println!("{:?}", data); // Requires Debug trait
}

This function requires that T implements both Printable and Debug traits.

Associated Types

Traits can define associated types, which are types that are associated with the trait. This allows you to define a trait that works with different types, but requires a specific type to be associated with it.

trait Iterator {
    type Item; // Associated type

    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    count: u32,
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 10 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let mut counter = Counter { count: 0 };
    while let Some(value) = counter.next() {
        println!("{}", value);
    }
}
  • Iterator trait defines an associated type Item.
  • Counter implements Iterator and specifies that Item is u32.
  • The next method returns an Option<Self::Item>, which is Option<u32> in this case.

Trait Objects

Trait objects allow you to work with values of different concrete types that implement the same trait, at runtime. They are created using a pointer (e.g., &dyn TraitName or Box<dyn TraitName>).

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

fn print_area(shape: &dyn Shape) {
    println!("Area: {}", shape.area());
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 4.0, height: 6.0 };

    print_area(&circle);      // Output: Area: 78.53981633974483
    print_area(&rectangle);   // Output: Area: 24
}
  • Shape trait defines an area method.
  • Circle and Rectangle implement Shape.
  • print_area takes a trait object &dyn Shape. This means it can accept any type that implements Shape.
  • The dyn keyword is crucial; it indicates dynamic dispatch (runtime polymorphism).

Important Considerations with Trait Objects:

  • Dynamic Dispatch: Trait objects use dynamic dispatch, which means the method call is resolved at runtime. This can be slower than static dispatch (used