Who’s never stumbled upon the tricky “the (…) trait bound is not satisfied” when coding in Rust? We have all been there!
This article will cover everything you need to know about Rust’s Traits, from the basics like Trait Bounds to more complex topics like Runtime Polymorphism and Trait Aliases.
Whether you’re just starting out or looking to brush up on your Rust skills, we’ve got you covered with clear explanations and practical examples to keep your Traits knowledge sharp!
What are Traits, at all?
Traits in Rust serve as a means to define shared behavior across different types. They specify a set of method signatures (and optionally provide default implementations) that implementing types must adhere to. This mechanism facilitates polymorphism and ensures a level of abstraction that enhances code reusability and maintainability.
Basic Trait Definition and Implementation
To begin, let’s define a simple Trait and implement it for a specific type:
trait Describable {
fn describe(&self) -> String;
}
struct Person {
name: String,
age: u8,
}
impl Describable for Person {
fn describe(&self) -> String {
format!("{} is {} years old.", self.name, self.age)
}
}
In this example, the Describable Trait requires a describe method. The Person struct implements this Trait, providing a concrete description of a person.
Default Methods and Overriding
Traits can provide default method implementations. Implementing types can use these defaults or override them:
trait Describable {
fn describe(&self) -> String {
String::from("This is an object.")
}
}
struct Product {
name: String,
price: f64,
}
// Using the default implementation
impl Describable for Product {}
struct Person {
name: String,
age: u8,
}
// Overriding the default implementation
impl Describable for Person {
fn describe(&self) -> String {
format!("{} is {} years old.", self.name, self.age)
}
}
Here, Product uses the default describe method from Describable, while Person provides its own implementation.
Advanced Trait Usage
As we delve deeper, Rust’s Trait system unveils more sophisticated capabilities such as Trait Bounds, Generic Constraints, and more.
Trait Bounds
Trait Bounds are a pivotal feature for working with generics, allowing you to specify that a generic type must implement a particular Trait:
fn print_description<T: Describable>(item: T) {
println!("{}", item.describe());
}
// Usage with a `Person` instance
let person = Person {
name: String::from("Alice"),
age: 30,
};
print_description(person);
This function can accept any type T as long as it implements the Describable Trait.
Multiple Trait Bounds
You can also specify multiple Traits using the + syntax or where clauses for more clarity and flexibility:
trait Identifiable {
fn identifier(&self) -> String;
}
// Using `+` for multiple Trait Bounds
fn print_detailed_description<T: Describable + Identifiable>(item: T) {
println!("{} - {}", item.identifier(), item.describe());
}
// Using `where` clauses
fn print_detailed_description<T>(item: T)
where
T: Describable + Identifiable,
{
println!("{} - {}", item.identifier(), item.describe());
}
Trait Objects
For runtime polymorphism, Rust provides Trait Objects. This allows for dynamic dispatch of methods at runtime, albeit with some performance cost:
fn print_descriptions(items: &[&dyn Describable]) {
for item in items {
println!("{}", item.describe());
}
}
let items: [&dyn Describable; 2] = [&person, &product];
print_descriptions(&items);
Here, dyn Describable is used to create a heterogeneous collection of items that implement the Describable Trait.
Associated Types and Traits
Traits can also define associated types, providing a way to associate one or more types with the Trait implementations:
trait Container {
type Item;
fn contains(&self, item: &Self::Item) -> bool;
}
struct Bag {
items: Vec<String>,
}
impl Container for Bag {
type Item = String;
fn contains(&self, item: &Self::Item) -> bool {
self.items.contains(item)
}
}
This pattern is particularly useful for Traits that need to work closely with other types.
Trait Inheritance
Traits in Rust can inherit from other Traits, allowing you to create a hierarchy of Traits. This is useful when a more specialized Trait should encompass all functionalities of a more generic one:
trait Named {
fn name(&self) -> String;
}
trait Employee: Named {
fn id(&self) -> u32;
}
struct Programmer {
name: String,
id: u32,
}
impl Named for Programmer {
fn name(&self) -> String {
self.name.clone()
}
}
impl Employee for Programmer {
fn id(&self) -> u32 {
self.id
}
}
In this example, the Employee Trait inherits from the Named Trait, meaning that any Employee must also implement the Named Trait, ensuring that an Employee can always provide a name as well as an ID.


