Define shared behavior with traits

Completed

A trait is a common interface that a group of types can implement. The Rust standard library has many useful traits, such as:

  • io::Read for values that can read bytes from a source.
  • io::Write for values that can write out bytes.
  • Debug for values that can be printed in the console using the "{:?}" format specifier.
  • Clone for values that can be explicitly duplicated in memory.
  • ToString for values that can be converted to a String.
  • Default for types that have a sensible default value, like zero for numbers, empty for vectors, and “” for String.
  • Iterator for types that can produce a sequence of values.

Each trait definition is a collection of methods defined for an unknown type, usually representing a capability or behavior that its implementors can do.

To represent the concept of "having a two-dimensional area," we can define the following trait:

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

Here, we declare a trait by using the trait keyword and then the trait's name, which is Area in this case.

Inside the braces, we declare the method signatures that describe the behaviors of the types that implement this trait, which in this case is the function signature fn area(&self) -> f64. The compiler will then check that each type implementing this trait must provide its own custom behavior for the body of the method.

Now, let's create some new types that will implement our Area trait:

struct Circle {
    radius: f64,
}

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

impl Area for Circle {
    fn area(&self) -> f64 {
        use std::f64::consts::PI;
        PI * self.radius.powf(2.0)
    }
}

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

To implement a trait for a type, we use the keywords impl Trait for Type, where Trait is the name of the trait being implemented and Type is the name of the implementor struct or the enum.

Within the impl block, we put the method signatures that the trait definition has required, filling the method body with the specific behavior that we want the methods of the trait to have for the particular type.

When a type implements a given trait, it's promising to uphold its contract. After implementing the trait, we can call the methods on instances of Circle and Rectangle in the same way we call regular methods, like this:

let circle = Circle { radius: 5.0 };
let rectangle = Rectangle {
    width: 10.0,
    height: 20.0,
};

println!("Circle area: {}", circle.area());
println!("Rectangle area: {}", rectangle.area());

You can interact with this code at this Rust Playground link.