Introduction
Trait bounds are a way to ensure type-safety of trait implementations, making the Code safer, and more predictable. We have actually already done something similar in Generics, where we have ensured that parameters being passed must be comparable.
fn compare_values<T>(value1: T, value2: T) -> bool
where
T: PartialEq,
{
value1 == value2
}
fn main() {
let result_int = compare_values(42, 42);
println!("{}", result_int); // true
let result_float = compare_values(3.14, 2.71);
println!("{}", result_float); // false
let result_str = compare_values("hello", "world");
println!("{}", result_str); // false
}
Examples
Let's see the below example to understand trait bounds.
trait Printable {
fn print(&self);
}
struct TestType;
impl Printable for TestType {
fn print(&self) {
println!("Printing Test Type.")
}
}
fn main() {
let test_type = TestType;
print_val(test_type);
}
fn print_val<T: Printable>(val: T) {
val.print();
}
So what is happening here? Let's see line-by-line.
We have defined a trait Printable and it only contains a print function. Then we have a struct named TestType and an implementation of the Printable trait for TestType struct where we're defining our print function. And as we can see it is only printing out 1 line.
Now comes the main function. Inside we're creating test_type variable of the type of TestType struct. We're then calling print_val function by passing test_type
The print_val function defines the input value using Generics, and it also specifies that the input must have the Printable trait. We're then calling the print function for the input value, because we already know that Printable traits have a print function.
If the print_val function accepted any input, there would be no surity that the input will be having the trait Printable, hence has the print function, and our Code will throw an error, which is avoided in this case. Very cool stuff!
Multiple Trait Bounds
Now we can also specify multiple trait bounds.
fn process<T: Printable + Clone>(val: T) {
let new_val = val.clone();
new_val.print();
}
So in this case we're specifying that the input val must have both the traits Printable & Clone. We're 1st cloning our input val, then calling the print function on the new_val
Where Clause
We can have multiple input values, and multiples traits to be specified for each value. Things will start getting messy if we try to write everything inline. Hence we can use the where clause:
fn process<T, U>(value: T, other: U)
where
T: Printable + Clone,
U: Display,
{
// Code ...
}
Conclusion
Trait bounds will undoubtedly make your working with traits 100x more fun, engaging, also safe. It only needs some practice. That's all for now, and cheers!