🦀 Rust Master Class - Chapter 12: Deep Dive into Traits
Struggling with Estonian cases, a stranger helped me. In return, I helped her with Rust traits. We didn't speak the same language, but traits are the great equalizer — they only care what you can do.
Traits in Rust define a set of methods that a type must provide to support shared behavior across different structures . They are a fundamental tool for polymorphism, allowing functions to operate on any type that implements a specific trait .
1. Basic Definition and Implementation
A trait defines a blueprint for methods. When a struct implements a trait, it must provide a concrete implementation for those methods .
Code Example:
trait GeneralInfo {
fn info(&self) -> (&str, u8, char);
}
struct Student {
name_std: String,
age: u8,
sex: char,
}
impl GeneralInfo for Student {
fn info(&self) -> (&str, u8, char) {
(&self.name_std, self.age, self.sex)
}
}
2. Static vs. Dynamic Dispatch
Rust provides two ways to use traits:
- Static Dispatch: Uses trait bounds (e.g.,
<T: Print>). The compiler generates specific code for each concrete type at compile time, leading to better performance . - Dynamic Dispatch: Uses trait objects (e.g.,
&dyn Print). The specific method to call is determined at runtime using a vtable. This allows for more flexibility, such as storing different types in a single vector, provided they all implement the same trait .
Code Example:
// Static Dispatch
fn static_display<T: Print>(value: T) { value.print(); }
// Dynamic Dispatch
fn display_dynamic(value: Vec<Box<dyn Print>>) {
for i in value { i.print(); }
}
3. Associated Types
Associated types allow a trait to define a placeholder type that is specified during implementation . This is often preferred over generics when a trait's implementation for a specific type will only ever use one concrete type .
Code Example:
trait DistanceThreeHours {
type Distance; // Placeholder type
fn distance_in_three_hours(&self) -> Self::Distance;
}
impl DistanceThreeHours for Kmh {
type Distance = Km;
fn distance_in_three_hours(&self) -> Self::Distance {
Km { value: self.value * 3 }
}
}
4. Super Traits and Marker Traits
- Super Traits: You can define a trait that requires another trait to be implemented first. For example, a
Studenttrait might require the type to also implement thePersontrait . - Marker Traits: These are traits with no methods, used only to provide information to the compiler about how a type can be used (e.g.,
Sized,Send,Sync) .
Code Example (Super Trait):
trait Person { fn name(&self) -> &str; }
trait Student: Person { // Student requires Person
fn complete_info(&self) -> (&str, u8, &str);
}
5. Operator Overloading
Rust allows you to implement standard operators (like +, -, *) for custom types by implementing specific traits from the std::ops module, such as Add, Sub, or Mul .
Code Example:
use std::ops::Mul;
impl Mul for Complex {
type Output = Complex;
fn mul(self, rhs: Complex) -> Self::Output {
Complex {
real: self.real * rhs.real - self.imag * rhs.imag,
imag: self.real * rhs.imag + self.imag * rhs.real,
}
}
}
6. Important Rules and Patterns
- Orphan Rule: You can only implement a trait for a type if either the trait or the type is local to your crate .
- Sealed Traits: A pattern used to prevent external crates from implementing a trait, ensuring that only the defining crate can provide implementations .
- Trait Aliases: An unstable feature that allows you to combine multiple traits into a single name for convenience (e.g.,
trait PrintableAndCalculable = Printable + Calculable;) . - Trait Object Limitations: For a trait to be "object safe" (used as
dyn Trait), its methods cannot have generic parameters or returnSelf, as the exact type is erased at runtime
📖 Download the full PDF: https://drive.google.com/file/d/1BtzPSU0XTeRJHnYO7JUS7EMvRwznfMLZ/view?usp=sharing
Part 12 of the Rust Master Class series — STEM EdTech | Automation Consulting | Rust Tutoring
Top comments (0)