Skip to Content
Course content

33: Defining and Implementing Traits

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a small payment module the other day, and I ran into a problem that perfectly illustrates why we need traits. I had a CreditCard struct and a PayPal struct. Both had a method called process_payment, but they were completely different types. I wanted to write a single function that could take any payment method and just run the process.

The Wall I Hit With Generics

My first instinct was to use a generic. I figured, "I'll just tell Rust the function takes some type T, and I'll call process_payment on it." Here is what I tried:

struct CreditCard { number: String }
struct PayPal { email: String }

impl CreditCard {
    fn process_payment(&self, amount: f64) {
        println!("Charging ${} to card {}", amount, self.number);
    }
}

impl PayPal {
    fn process_payment(&self, amount: f64) {
        println!("Charging ${} to PayPal account {}", amount, self.email);
    }
}

fn execute_payment<T>(method: T, amount: f64) {
    method.process_payment(amount);
}

I hit cargo build and the compiler immediately yelled at me. It basically said: "I have no idea what T is. For all I know, T could be an integer or a boolean, and neither of those have a process_payment method."

Even though I knew I'd only ever pass in a CreditCard or PayPal, Rust requires absolute certainty. I can't just assume a method exists; I have to prove it.

Defining the Contract

This is where traits come in. A trait is essentially a contract. Instead of telling Rust "this is some type T," I can tell it "this is some type T that implements the PaymentProcessor trait."

First, I need to define what that contract looks like. I don't write the logic here; I just define the signature of the methods that any "payment processor" must have:

trait PaymentProcessor {
    fn process_payment(&self, amount: f64);
}

Now, my existing structs don't actually follow this contract yet. They just happen to have methods with the same name. I have to explicitly implement the trait for each one. Note that the syntax changes slightly—instead of impl CreditCard, it's impl PaymentProcessor for CreditCard.

impl PaymentProcessor for CreditCard {
    fn process_payment(&self, amount: f64) {
        println!("Charging ${} to card {}", amount, self.number);
    }
}

impl PaymentProcessor for PayPal {
    fn process_payment(&self, amount: f64) {
        println!("Charging ${} to PayPal account {}", amount, self.email);
    }
}

Tying It All Together

Now for the magic part. I can go back to my execute_payment function. But this time, I'm going to use a trait bound. I'll tell Rust that T must implement PaymentProcessor.

fn execute_payment<T: PaymentProcessor>(method: T, amount: f64) {
    method.process_payment(amount);
}

Wait, I actually prefer a cleaner syntax when the bounds get long. You'll often see the where clause instead, which does the exact same thing but keeps the function signature from getting cluttered:

fn execute_payment<T>(method: T, amount: f64) 
where 
    T: PaymentProcessor 
{
    method.process_payment(amount);
}

Now, if I try to pass a String into execute_payment, the compiler will stop me. But if I pass a CreditCard or PayPal, it works perfectly. I've effectively decoupled the execution logic from the specific payment implementation. If I decide to add CryptoPayment tomorrow, I don't have to touch the execute_payment function at all—I just implement the trait for the new struct.




📋 Practical Task

Implementing a VolumeCalculator for Geometric Shapes

You are building a 3D modeling tool. You need to calculate the total volume of a scene containing various shapes.

  • Define a trait called VolumeCalculator with a single method calculate_volume(&self) -> f64.
  • Create two structs: Cube (with a side field) and Sphere (with a radius field).
  • Implement the VolumeCalculator trait for both structs.
    • Hint: Volume of a cube is side³, volume of a sphere is (4/3) * π * radius³. You can use std::f64::consts::PI.
  • Write a generic function called print_volume<T: VolumeCalculator>(shape: T) that calls the calculate_volume method and prints the result to the console.
  • In your main function, instantiate one cube and one sphere, and pass both to print_volume.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.