Rust Trait by Example
I’m excited to share a couple of topics on effectively utilizing traits in Rust.
Supertrait
In Rust, a supertrait is a trait that imposes additional requirements beyond those specified by another trait. A supertrait extends the functionality of a trait by adding more methods or functionality that the implementing types must fulfill. It establishes a relationship where any type implementing the subtrait must also implement its supertrait.
For example:
trait Area {
fn calculate_area(&self) -> f64;
}
// Area is a supertrait of Shape, Shape is a subtrait of Area
trait Shape: Area {
fn describe(&self);
}
struct Rectangle {
length: f64,
width: f64,
}
impl Area for Rectangle {
fn calculate_area(&self) -> f64 {
self.length * self.width
}
}
// To impl Shape we must impl its supertrait Area like above
impl Shape for Rectangle {
fn describe(&self) {
println!("I am a rectangle with length {} and width {}", self.length, self.width);
// We can access supertrait's method using self
println!("Area: {}", self.calculate_area());
}
}
fn print_shape_info<T: Shape>(shape: &T) {
shape.describe();
}
fn main() {
let rectangle = Rectangle { length: 4.0, width: 3.0 };
print_shape_info(&rectangle);
// Output:
// I am a rectangle with length 4 and width 3
// Area: 12
}
Trait Extension
Trait extension in Rust allows you to add new methods or behaviour to existing types by implementing traits for those types. It’s a powerful concept that enables you to extend the functionality of types without modifying their original definitions.
Here’s an example to illustrate trait extension:
// Define a trait with a method
trait Hello {
fn say_hello(&self);
}
// Implement the trait for the type i32
impl Hello for i32 {
fn say_hello(&self) {
println!("Hello, I'm {}!", self);
}
}
fn main() {
let number = 42;
number.say_hello(); // Calling the method from the trait extension
}
Blanket Implementation
Blanket Implementation is a generic extension that allows you to conditionally implement a trait for any type that implements another trait:
use std::fmt::Debug;
trait Displayable {
fn display(&self);
}
impl<T: Debug> Displayable for T {
fn display(&self) {
println!("Hello {:?}", self);
}
}
#[derive(Debug)]
struct MyStruct {
value: i32,
}
fn main() {
let instance = MyStruct { value: 42 };
instance.display(); // This will print: Hello MyStruct { value: 42 }
}
Method Disambiguation
Rust compiler prevents ambiguity in method resolution when a type implements multiple traits with conflicting methods.
Here is an example that introduces this scenario and corresponding solutions:
- Explicit casting to access a specific trait’s methods
- Using a trait object
trait A {
fn greet(&self) -> &'static str {
"Hello from A"
}
}
trait B {
fn greet(&self) -> &'static str {
"Hello from B"
}
}
struct C;
impl A for C {}
impl B for C {}
fn main() {
let c = C;
// println!("{}", c.greet()); // Compiling error: multiple `greet` found
// Option 1: Explicit casting to access a specific trait's methods
println!("{}", A::greet(&c)); // Output: "Hello from A"
println!("{}", B::greet(&c)); // Output: "Hello from B"
// Option 2: Convert c to a trait object of type A
let a: &dyn A = &c;
println!("{}", a.greet()); // Output: "Hello from A"
}