loading...

Rust - understanding traits 1

brunooliveira profile image Bruno Oliveira ・4 min read

Introduction

Many languages today are object-oriented, support this paradigm or, not being object-oriented directly, allow for the programmer to define its own data types.

Usually, we want these types to exhibit certain traits of behavior under certain circumstances, for example, when printing the types to standard output, or when making it possible to iterate over a certain type, we need to know how to perform these operations. These are defined by making sure that our types implement certain methods, usually provided by a trait.

Traits

A trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait.

Traits can be implemented for any data type. In the example below, we define Animal, a group of methods:

trait Animal {
    // Static method signature; `Self` refers to the implementor type.
    fn new(name: &'static str) -> Self;

    // Instance method signatures; these will return a string.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    // Traits can provide default method definitions.
    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

Having defined this trait, we can now make our own types implement this trait, which means that they will have all the behavior of an animal, while still adding their own functionality.

As examples of animals, we can define two completely distinct animal types: a StuffedAnimal and a Cat. Both are animals, so, they share certain common characteristics, like having a name, talking and making noises, but the way they do it will differ whether we are referring to a stuffed animal or to a real cat. Let's see how we can make our types implement a certain trait.

Making a custom type implement a trait

To start, we first define two very basic custom types that represent the concepts we will want to illustrate, a stuffed animal and a cat:

struct Cat {
    name: &'static str,
    age: i32
}

struct StuffedAnimal {
    name: &'static str
}

A cat here is simply represented by a name and its age, while a stuffed animal has simply a name.

For now, these types can be instantiated and used as follows:

fn main() {
  let c=Cat{name: "Bobi",age:8};
  println!("Cat's name is {} and he is {} years old",c.name, c.age);
}

So, we can leverage the attributes that represent our types to work with a higher level representation of a certain concept in code.

With traits, this idea can be expanded upon, because in essence, our types will exhibit the behavior defined by the trait that they implement. In our particular case, we will be able to "look at" our types as "animals", since they will be able to talk with a specific noise. It will also be possible to instantiate them using the new() method we see above, which will simply reference the "hand-made" struct creation we see above.

Here's how to implement a trait for a type:

impl Animal for Cat {
    // `Self` is the implementor type: `Cat`.
    fn new(name: &'static str) -> Cat {
        Cat { name: name, age: 1 }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        "Meowww"
    }

    // Default trait methods can be overridden.
    fn talk(&self) {
        // For example, we can add some quiet contemplation.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

The syntax to use here is of type: impl <trait name> for <type name> as the header declaration, and, like many other languages, in the definition and actual trait implementation, we define the implementation for the methods that we want to have according to the underlying implementation. As we can see, a cat meows when making noise.

Another important thing to notice here is that we can override default trait methods, in this particular example, we override the talk() method, for the specific case of a cat.

So, this is how we can define a trait for a user-defined type.

Full example

As a full example, we can also implement the trait for a stuffed animal:

trait Animal {
    // Static method signature; `Self` refers to the implementor type.
    fn new(name: &'static str) -> Self;

    // Instance method signatures; these will return a string.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    // Traits can provide default method definitions.
    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

struct Cat {
    name: &'static str,
    age: i32
}

struct StuffedAnimal {
    name: &'static str
}

impl Animal for Cat {
    fn new(name: &'static str) -> Cat {
        Cat { name: name, age: 1 }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        "Meowww"
    }

    // Default trait methods can be overridden.
    fn talk(&self) {
        // For example, we can add some quiet contemplation.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

impl Animal for StuffedAnimal {
    fn new(name: &'static str) -> StuffedAnimal {
        StuffedAnimal { name: name}
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        "<random factory noise>"
    }
}

fn main() {
  let c: Cat=Animal::new("Bobi");
  println!("Cat's name is {} {} ",c.name(), c.age);
  c.talk();

  let stuffed: StuffedAnimal=Animal::new("BobiStuffed");
  stuffed.talk();
}

So, we can see when we run this, that depending on the implementation of the trait, we can get different behavior. Note also that the talk method returns the default implementation on the second example.

Conclusion

After this basic introduction to traits, you should be able to add custom trait implementations for your own types and make your types more versatile and closer to your problem domains! Stay tuned for more Rust!

Discussion

markdown guide