Using traits with generics – trait bounds

Now that we have a decent idea about generics and traits, we can explore ways in which we can combine them to express more about our interfaces at compile time. Consider the following code:

// trait_bound_intro.rs

struct Game;
struct Enemy;
struct Hero;

impl Game {
fn load<T>(&self, entity: T) {
entity.init();
}
}

fn main() {
let game = Game;
game.load(Enemy);
game.load(Hero);
}

In the preceding code, we have a generic function, load, on our Game type that can take any game entity and load it in our game world by calling init() on all kinds of T. However, this example fails to compile with the following error:

So, a generic function taking any type T cannot know or assume by default  the init method exists on T. If it did, it wouldn't be generic at all, and would only be able to accept types that have the init() method on them. So, there is a way that we can let the compiler know of this and constrain the set of types that load can accept using traits. This is where trait bounds come into the picture. We can define a trait called Loadable and implement it on our our Enemy and Hero types. Following that, we have to put a couple of symbols beside our generic type declaration to specify the trait. We call this a trait bound. The changes to the code are as follows:

// trait_bounds_intro_fixed.rs

struct Game;
struct Enemy;
struct Hero;

trait Loadable {
fn init(&self);
}

impl Loadable for Enemy {
fn init(&self) {
println!("Enemy loaded");
}
}

impl Loadable for Hero {
fn init(&self) {
println!("Hero loaded");
}
}

impl Game {
fn load<T: Loadable>(&self, entity: T) {
entity.init();
}
}

fn main() {
let game = Game;
game.load(Enemy);
game.load(Hero);
}

In this new code, we implement Loadable for both Enemy and Hero and we also modified the load method as follows:

fn load<T: Loadable>(&self, entity: T) { .. }

Notice the : Loadable part. This is how we specify a trait bound. Trait bounds allow us to constrain the range of parameters that a generic API can accept. Specifying a trait bound on a generic item is similar to how we specify types for variables, but here the variable is the generic type T and the type is some trait, such as T: SomeTrait. Trait bounds are almost always needed when defining generic functions. If one defines a generic function that takes T without any trait bounds, we cannot call any of the methods since Rust does not know what implementation to use for the given method. It needs to know whether T has the foo method or not to monomorphize the code. Take a look at another example:

// trait_bounds_basics.rs

fn add_thing<T>(fst: T, snd: T) {
let _ = fst + snd;
}

fn main() {
add_thing(2, 2);
}

We have a method,  add_thing, that can add any type T. If we compile the preceding snippet, it does not compile and gives the following error:

It says to add a trait bound Add on T. The reason for this is that the addition operation is dictated by the Add trait, which is generic, and different types have different implementations that might even return a different type altogether. This means that Rust needs our help to annotate that for us. Here, we need to modify our function definition like so:

// trait_bound_basics_fixed.rs

use std::ops::Add;

fn add_thing<T: Add>(fst: T, snd: T) {
let _ = fst + snd;
}

fn main() {
add_thing(2, 2);
}

We added the : Add after T and with that change, our code compiles. Now, there are two ways to specify trait, bounds depending on how complex the type signature gets when defining generic items with trait bounds:

In-between generics:

fn show_me<T: Display>(val: T) {
// can use {} format string now, because of Display bound
println!("{}", val);
}

This is the most common syntax to specify trait bounds on generic items. We read the preceding function as follows show_me is a method that takes any type that implements the Display trait. This is the usual syntax used to declare the trait bound when the length of the type signature of the generic function is small. This syntax also works when specifying trait bounds on types. Now, let's look at the second way to specify trait bounds.

Using where clauses:

This syntax is used when the type signature of any generic item becomes too large to fit on a line. For example, there is a parse method in the standard library's std::str module, which has the following signature:

pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>
where F: FromStr { ... }

Notice the where F: FromStr part. This tells us that our F type must implement the FromStr trait. The where clause decouples the trait bound from the function signature and makes it readable.

Having seen how to write trait bounds, it's important to know where can we specify these bounds. Trait bounds are applicable in all of the places where you can use generics.

..................Content has been hidden....................

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