Traits

A trait is an item that defines a set of contracts or shared behavior that types can opt to implement. Traits are not usable by themselves and are meant to be implemented by types. Traits have the power to establish relationships between distinct types. They are the backbone to many language features such as closures, operators, smart pointers, loops, compile-time data race checks, and much more. Quite a few of the high-level language features in Rust boil down to some type calling a trait method that it implements. With that said, let's look at how we can define and use a trait in Rust!

Let's say we are modeling a simple media player application that can play audio and video files. For this demo, we'll create a project by running cargo new super_player. To convey the idea of traits and to make this simple, in our main.rs file, we have represented our audio and video media as tuple structs with the name of the media as a String, like so:

// super_player/src/main.rs

struct Audio(String);
struct Video(String);

fn main() {
// stuff
}

Now, at the very minimum, both the Audio and Video structs need to have a play and pause method. It's a functionality that's shared by both of them. It's a good opportunity for us to use a trait here. Here, we'll define a trait called Playable with two methods in a separate module called media.rs, like so:

// super_player/src/media.rs

trait Playable {
fn play(&self);
fn pause() {
println!("Paused");
}
}

We use the trait keyword to create a trait, followed by its name and a pair of braces. Within the braces, we can provide zero or more methods that any type implementing the trait should fulfill. We can also define constants within traits, which all of the implementers can share. The implementer can be any struct, enum, primitive, function, closure, or even a trait.

You may have noticed the signature of play; it takes a reference to a symbol, self, but does not have a body, and ends with a semicolon.  self is just a type alias to Self, which refers to the type on which the trait is being implemented. We'll cover these in detail in Chapter 7,  Advanced Concepts. This means that the methods within the traits are like an abstract method from Java. It is up to the types to implement this trait and define the function according to their use case. However, methods declared within a trait can also have default implementations, as is the case with the pause function in the preceding code. pause does not take self, and so it's akin to a static method that does not require an instance of the implementer to invoke it.

We can have two kinds of methods within a trait:

  • Associated methods: These are methods that are available directly on the type implementing the trait and do not need an instance of the type to invoke them. There are also known as static methods in mainstream languages, for example, the from_str method from the FromStr trait in the standard library. It is implemented for a String and thus allows you to create a String from a &str by calling String::from_str("foo").
  • Instance methods: These are methods that have their first parameter as self. These are only available on instances of the type that are implementing the trait.  self points to the instance of the type implementing the trait. It can be of three types: self  methods, which consume the instance when called; &self methods, which only have read access to the instance its members (if any); and &mut self methods, which have mutable access to its members and can modify them or even replace them with another instance. For example, the as_ref method from the AsRef trait in the standard library is an instance method that takes &self, and is meant to be implemented by types that can be converted to a reference or a pointer. We'll cover references and the & and &mut parts of the type signature in these methods when we get to Chapter 5, Memory Management and Safety.

Now, we'll implement the preceding Playable trait on our Audio  and Video types, like so:

// super_player/src/main.rs

struct Audio(String);
struct Video(String);

impl Playable for Audio {
fn play(&self) {
println!("Now playing: {}", self.0);
}
}

impl Playable for Video {
fn play(&self) {
println!("Now playing: {}", self.0);
}
}

fn main() {
println!("Super player!");
}

We write trait implementations with the impl keyword followed by the trait name, followed by the for keyword and the type we want to implement the trait for, followed by a pair of braces. Within these braces, we are required to provide the implementations of methods, and optionally override any default implementation that exists in the trait. Let's compile this:

The preceding error highlights an important feature of traits: traits are private by default. To be usable by other modules or across crates, they need to be made public. There are two steps to this. First, we need to expose our trait to the outside world. To do that, we need to prepend our Playable trait declaration with the pub keyword:

// super_player/src/media.rs

pub trait Playable {
fn play(&self);
fn pause() {
println!("Paused");
}
}

After we have exposed our trait, we need to use the use keyword to bring the trait into scope in the module we want to use the trait in. This will allow us to call its methods, like so:

// super_player/src/main.rs

mod media;

struct Audio(String);
struct Video(String);

impl Playable for Audio {
fn play(&self) {
println!("Now playing: {}", self.0);
}
}

impl Playable for Video {
fn play(&self) {
println!("Now playing: {}", self.0);
}
}

fn main() {
println!("Super player!");
let audio = Audio("ambient_music.mp3".to_string());
let video = Video("big_buck_bunny.mkv".to_string());
audio.play();
video.play();
}

With that, we can play our audio and video media:

This is very far from any actual media player implementation, but our aim was to explore the use case for traits.

Traits can also specify in their declaration that they depend on other traits; this is a feature known as trait inheritance. We can declare inherited traits like so:

// trait_inheritance.rs

trait Vehicle {
fn get_price(&self) -> u64;
}

trait Car: Vehicle {
fn model(&self) -> String;
}

struct TeslaRoadster {
model: String,
release_date: u16
}

impl TeslaRoadster {
fn new(model: &str, release_date: u16) -> Self {
Self { model: model.to_string(), release_date }
}
}

impl Car for TeslaRoadster {
fn model(&self) -> String {
"Tesla Roadster I".to_string()
}
}

fn main() {
let my_roadster = TeslaRoadster::new("Tesla Roadster II", 2020);
println!("{} is priced at ${}", my_roadster.model, my_roadster.get_price());
}

In the preceding code, we declared two traits: a Vehicle (a more general) trait and a Car (more specific) trait, which depends on Vehicle. Since TeslaRoadster is a car, we implemented the Car trait for it. Also, notice the body of the method new on TeslaRoadster, which uses Self as the return type. This is also substituted for the TeslaRoadster instance that we return from new. Self is just a convenient type alias for the implementing type within the trait's impl blocks. It can also be used to create other types, such as tuple structs and enums, and also in match expressions. Let's try compiling this code:

See that error? In its definition, the Car trait specifies the constraint that any type that implements the trait must also implement the Vehicle trait, Car: Vehicle. We did not implement Vehicle for our TeslaRoadster, and Rust caught and reported it for us. Therefore, we must implement the Vehicle trait like so:

// trait_inheritance.rs

impl Vehicle for TeslaRoadster {
fn get_price(&self) -> u64 {
200_000
}
}

With that implementation satisfied, our program compiles fine with the following output:

 Tesla Roadster II is priced at $200000
The underscore in 200_200 in the get_price method is a handy syntax to create readable numeric literals.

As an analogy to object-oriented languages, traits and their implementations are similar to interfaces and classes that implement those interfaces. However, it is to be noted that traits are very different from interfaces:

  • Even though traits have a form of inheritance in Rust, implementations do not. This means that a trait called Panda can be declared, which requires another trait called KungFu to be implemented by types that implement Panda. However, the types themselves don't have any sort of inheritance. Therefore, instead of object inheritance, type composition is used, which relies on trait inheritance to model any real-world entity in code.
  • You can write trait implementation blocks anywhere, without having access to the actual type.
  • You can also implement your own traits on any type ranging from built-in primitive types to generic types.
  • You cannot implicitly have return types as traits in a function like you can return an interface as a return type in Java. You have to return something called a trait object, and the syntax to do that is explicit. We'll see how to do that when we get to trait objects.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset