2.19 Traits

Rust’s traits are data structures defining certain functionality any struct implementing it must provide: referring to what functions any given struct implementing the trait must provide.

Let us set up a quick struct for storing data according to a person:

struct Person
{
    age: i32, 
    name: String
}

impl Person
{
    pub fn new() -> Person
    {
        Person { age: 26, name: "Bob" }
    }
}

This struct contains information about the age and name of a person. It contains a constructor initializing it to a set of default values. Keep in mind that the constructor needs to be pub, as we need to be able to access it from outside the struct itself.

Since all people have beards, even women, although the potentially small amount usually is shaved off before getting out of hand, we are going to define a trait which makes sure there is a method to retrieve the beard color of a person:

trait HasBeard
{
    fn beard_color(&self) -> String;
}

This trait makes it required for any struct implementing it to contain a function by the name of beard_color. The trait describes any functionality that is required of types implementing it, using simplified versions of the functions lacking a function body.

You may put the trait wherever you would like in your code: it is, however, recommended that you put it somewhere close to the structs using it.

Implementation

Let us implement HasBeard for Person. In order for this, create another impl that contains the information about our implementation:

impl HasBeard for Person
{
    fn beard_color(&self) -> String
    {
        "Pink".to_string()
    }
}

See how the part after the impl looks slightly different from the other one? We implement traits in the order of writing the trait out first, considering they are what we are implementing for a certain struct.

Since we are using the HasBeard trait, we are required to provide the implementation with a beard_color method. In this case, that method always returns “Pink”. Because surely everyone colors their beards pink.

If we had multiple traits that we wished to implement for a certain struct, we would need to do one impl block for each trait we want to have implemented.

Self

There is another new keyword that pops into the above example: &self. self is a way of referring to the struct which the method we are modifying is located in. If we had some variables stored inside that struct, we can access them using self.

There are three different ways you may use self, and these are the ways of any type in Rust: self, &self and &mut self. The type you use for the self depends on what type of access you would like to the parent struct.

self is put at the beginning of a method implementation.

Running it

We can then take this one step further and write an actual program out of it:

main
{
    let person = Person::new();

    println!("The person has a beard color of {}.", person.beard_color());
}

This program will quite nicely print out that “The person has a beard color of Pink.” How wonderful!

Generic function trait boundaries

What if we had a function that needs to take a struct implementing a certain trait? We can accomplish that using trait boundaries:

fn print_beard_color<T>(person: T)
{
    println!("The person has a beard color of {}.", person.beard_color());
}

As of right now, this function will not work as we cannot guarantee that the type of person contains a function called beard_color. But we can fix that:

fn print_beard_color<T: HasBeard>(person: T)
{
    println!("The person has a beard color of {}.", person.beard_color());
}

Rather than using just T for the generic elements, we give T a type of HasBeard, meaning we tell Rust that T can be of any type as long as it implements the HasBeard trait.

Let us modify our main function real fast:

main
{
    let person = Person::new();

    print_beard_color(person);
}

Did you notice how nice and simple that was? Now we can call a function that takes only types implementing the HasBeard trait, to print the beard color of that instance: which happens to be pink for all cases.

Generic struct trait boundaries

You can even put boundaries on generic structs, in order to only do a certain implementation for a type containing a certain trait:

struct Person<T>
{
    age: T, 
    beard_length: T
}

impl<T: PartialEq> Person<T>
{
    fn is_equal(&self) ->
    {
        self.age == self.beard_length
    }
}

The above implies, since T needs to correspond with the PartialEq trait, that age and beard_length can both be compared in value. Should they not be able to, the implementation will not take place.

Traits for primitive types

Although considered poor style, it is highly possible implementing traits for any primitive type:

impl HasBeard for u32
{
    fn beard_color(&self) -> String
    {
        "Red".to_string()
    }
}

You can be sure that all u32s have a red beard, otherwise why would they begin on a ‘u’?

Rules

There is one huge rule when it comes to implementing traits: either the struct you are implementing the trait for or the trait, must be contained by your own code, otherwise Rust will generate you with an error.

The reason as to why there is this rule is to make sure the programmer does not mess around, putting security at Risk. Remember how Rust is an extremely safe language in and of itself.

Exercises

  • Create a struct to symbolize a hot lady model.
  • Define a trait which adds a poke method on the hot model lady. Make sure she gets real upset upon poking her.
  • Stop thinking about whatever you are thinking about.

Conclusion

With a little help from traits, we can create really interesting data structures. Traits are Rust’s super- respectively subclasses. This is how you make multiple structures to have something in common with one an other.

Format