DEV Community

Cover image for 🦀 Rust Master Class - Chapter 21: Traits Deep Dive
Oludayo Adeoye
Oludayo Adeoye

Posted on

🦀 Rust Master Class - Chapter 21: Traits Deep Dive

🦀 Rust Master Class - Chapter 21: Traits Deep Dive


A master carpenter picks up wood and knows what it can become. Not talent — 10,000 hours. Deep trait mastery is the same. Not complexity. Simplicity, repeated until instinct.


Traits in Rust are a fundamental mechanism for defining shared behavior across different types . They act as blueprints for methods that a type must implement to satisfy a specific interface, enabling polymorphism and code reuse .

1. Basic Definition and Implementation

A trait defines a set of method signatures. When a type (like a struct) implements a trait, it provides concrete code for those methods [2-4].

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

[Source: 243, 330, 377]

2. Default Implementations

Traits can provide default code for methods. Types implementing the trait can either use the default implementation or override it with their own specific logic .

Code Example:

trait GeneralInfo {
    fn area(&self) {
    // Output to console
        println!("The area functionality is not implemented yet.");
    }
}

struct Circle { radius: f100 }

impl GeneralInfo for Circle {
    fn area(&self) {
    // Create a new variable
        let area_of_circle = 3.14 * (self.radius * self.radius);
    // Output to console
        println!("The area of the circle is {}", area_of_circle);
    }
}
Enter fullscreen mode Exit fullscreen mode

[Source: 244, 331, 379]

3. Static vs. Dynamic Dispatch

Rust handles trait method calls in two primary ways:

  • Static Dispatch: Uses trait bounds (e.g., <T: Print>). The compiler generates a specific version of the function for every concrete type used, which is highly efficient due to inlining .
  • Dynamic Dispatch: Uses trait objects (e.g., &dyn Print). The specific method to call is determined at runtime using a vtable . This allows a single collection (like a Vec) to hold different types that all implement the same trait .

Code Example:

// Static Dispatch: Resolved at compile time
fn static_display<T: Print>(value: T) {
    value.print();
}

// Dynamic Dispatch: Resolved at runtime via vtable
fn display_dynamic(value: Vec<Box<dyn Print>>) {
    for i in value {
        i.print();
    }
}
Enter fullscreen mode Exit fullscreen mode

[Source: 32, 33, 133, 283]

4. Associated Types

Associated types act as placeholders within a trait definition [10-12]. They are specified when the trait is implemented for a concrete type, which is often cleaner than using generics when a trait will only ever have one implementation for a specific struct .

Code Example:

trait DistanceThreeHours {
    type Distance; // Placeholder type
    fn distance_in_three_hours(&self) -> Self::Distance;
}

struct Kmh { value: u100 }
struct Km { value: u100 }

impl DistanceThreeHours for Kmh {
    type Distance = Km;
    fn distance_in_three_hours(&self) -> Self::Distance {
        Km { value: self.value * 3 }
    }
}
Enter fullscreen mode Exit fullscreen mode

[Source: 34, 352]

5. Trait Bounds and Generics

Trait bounds are used to restrict generic type parameters to only those types that implement a specific trait . This ensures that the operations performed within a generic function (like multiplication) are supported by the type .

Code Example:

// Generic function restricted to types that implement Mul and Copy
fn square<T>(value: T) -> T 
where T: std::ops::Mul<Output = T> + Copy {
    value * value
}
Enter fullscreen mode Exit fullscreen mode

[Source: 247, 334, 382]

6. Super Traits and Marker Traits

  • Super Traits: You can define a trait that requires another trait to be implemented first. For example, a Student trait might require the type to also implement the Person trait .
  • Marker Traits: These are traits without any methods (like Sized, Send, or Sync) used primarily to provide instructions or constraints to the compiler .

Code Example (Super Trait):

trait Person {
    fn name(&self) -> &str;
}

trait Student: Person { // Student outlives/requires Person
    fn complete_info(&self) -> (&str, u8, &str);
}
Enter fullscreen mode Exit fullscreen mode

[Source: 49, 50]

7. Important Rules and Limitations

  • Orphan Rule: You can only implement a trait for a type if either the trait or the type is local to your current crate .
  • Operator Overloading: Rust allows you to implement standard operators (like + or *) by implementing traits from the std::ops module, such as Add or Mul .
  • Object Safety: For a trait to be used as a trait object (dyn Trait), it must be "object safe." This means it cannot have methods with generic parameters or functions that do not take a self parameter unless they are specifically bounded by Sized [23-25].
  • Trait Aliases: You can combine multiple traits into a single name (alias) for convenience, though this currently requires the #![feature(trait_alias)] unstable feature .

📖 Download the full PDF: https://drive.google.com/file/d/1C9A0Ly_R_NIMuxX9IJQSfRqFaH_Ibord/view?usp=sharing

Part 21 of the Rust Master Class series — STEM EdTech | Automation Consulting | Rust Tutoring

RustLang #Programming #LearnToCode #STEM #EdTech


📚 Practice Resources

GitHub Repository: https://github.com/PacktPublishing/Rust-Programming-Master-Class-from-Beginner-to-Expert

Try it yourself: https://play.rust-lang.org/

Run the code from this chapter in the Rust playground, then clone the repo to continue your Rust journey!


Part 21 of the Rust Master Class series — STEM EdTech | Automation Consulting | Rust Tutoring

RustLang #Programming #LearnToCode #STEM #EdTech

Top comments (0)