Debug School

rakesh kumar
rakesh kumar

Posted on

How runtime polymorphism implement using dyn traits

Returning traits with dyn (Trait Objects) in Rust

Theory (why it’s needed)

In Rust, traits are not concrete types. A function must return one concrete sized type.

But sometimes you want:

to return different concrete types from the same function (based on if, config, feature flags)

to store mixed types in one collection (Vec<...>) as long as they share behavior

That’s where trait objects come in:

Box = “a heap-allocated value of some type that implements Trait”

Uses dynamic dispatch (runtime method lookup), like interface calls in Java

Needed because different concrete types have different sizes, but Box has a fixed size (a pointer + vtable)

Important comparison:

-> impl Trait can return only one concrete type

-> Box<dyn Trait> can return many different types
Enter fullscreen mode Exit fullscreen mode

Rust: Full code (shows why dyn is needed)

use std::fmt;

// A trait = behavior contract
trait Notifier {
    fn notify(&self, msg: &str);
}

// Two different concrete types implementing the same trait
struct EmailNotifier {
    to: String,
}

struct SmsNotifier {
    phone: String,
}

impl Notifier for EmailNotifier {
    fn notify(&self, msg: &str) {
        println!("EMAIL to {} => {}", self.to, msg);
    }
}

impl Notifier for SmsNotifier {
    fn notify(&self, msg: &str) {
        println!("SMS to {} => {}", self.phone, msg);
    }
}

// ✅ Returning a TRAIT OBJECT: can return different concrete types
fn build_notifier(kind: &str) -> Box<dyn Notifier> {
    match kind {
        "email" => Box::new(EmailNotifier {
            to: "ashwani@example.com".to_string(),
        }),
        _ => Box::new(SmsNotifier {
            phone: "+91-99999-11111".to_string(),
        }),
    }
}

// ✅ Also useful to store mixed types in one Vec
fn demo_vec_of_trait_objects() {
    let list: Vec<Box<dyn Notifier>> = vec![
        Box::new(EmailNotifier {
            to: "a@demo.com".to_string(),
        }),
        Box::new(SmsNotifier {
            phone: "+91-88888-22222".to_string(),
        }),
    ];

    for n in list {
        n.notify("Server is UP");
    }
}

// ❌ Why impl Trait doesn't work here (explanation)
// fn build_notifier_impl(kind: &str) -> impl Notifier {
//     if kind == "email" {
//         EmailNotifier { to: "...".to_string() }
//     } else {
//         SmsNotifier { phone: "...".to_string() } // ERROR: different type
//     }
// }

fn main() {
    println!("--- Returning traits with dyn ---");
    let n1 = build_notifier("email");
    let n2 = build_notifier("sms");

    n1.notify("Payment received");
    n2.notify("OTP: 123456");

    println!("\n--- Vec<Box<dyn Trait>> example ---");
    demo_vec_of_trait_objects();
}
Enter fullscreen mode Exit fullscreen mode

Output (Rust)

--- Returning traits with dyn ---
EMAIL to ashwani@example.com => Payment received
SMS to +91-99999-11111 => OTP: 123456

--- Vec<Box<dyn Trait>> example ---
EMAIL to a@demo.com => Server is UP
SMS to +91-88888-22222 => Server is UP
Enter fullscreen mode Exit fullscreen mode

What exactly is happening (simple explanation)

EmailNotifier and SmsNotifier have different sizes

Rust cannot return “sometimes EmailNotifier, sometimes SmsNotifier” directly

Box has a fixed size pointer

Inside the box is the actual object

dyn Notifier uses a vtable so at runtime it knows which notify() to call

Java comparison (interfaces = Rust trait objects)

Java’s interface references are basically like Box:

you can return different classes as the interface type

you can store mixed implementations in a list

Java: Full code + output

import java.util.*;

public class Main {

    interface Notifier {
        void notify(String msg);
    }

    static class EmailNotifier implements Notifier {
        String to;
        EmailNotifier(String to) { this.to = to; }
        public void notify(String msg) {
            System.out.println("EMAIL to " + to + " => " + msg);
        }
    }

    static class SmsNotifier implements Notifier {
        String phone;
        SmsNotifier(String phone) { this.phone = phone; }
        public void notify(String msg) {
            System.out.println("SMS to " + phone + " => " + msg);
        }
    }

    // Return interface type (same idea as Box<dyn Notifier>)
    static Notifier buildNotifier(String kind) {
        if ("email".equals(kind)) {
            return new EmailNotifier("ashwani@example.com");
        } else {
            return new SmsNotifier("+91-99999-11111");
        }
    }

    static void demoList() {
        List<Notifier> list = Arrays.asList(
            new EmailNotifier("a@demo.com"),
            new SmsNotifier("+91-88888-22222")
        );

        for (Notifier n : list) {
            n.notify("Server is UP");
        }
    }

    public static void main(String[] args) {
        System.out.println("--- Returning interface ---");
        Notifier n1 = buildNotifier("email");
        Notifier n2 = buildNotifier("sms");

        n1.notify("Payment received");
        n2.notify("OTP: 123456");

        System.out.println("\n--- List<Notifier> example ---");
        demoList();
    }
}
Enter fullscreen mode Exit fullscreen mode

Output (Java)

--- Returning interface ---
EMAIL to ashwani@example.com => Payment received
SMS to +91-99999-11111 => OTP: 123456

--- List<Notifier> example ---
EMAIL to a@demo.com => Server is UP
SMS to +91-88888-22222 => Server is UP

Enter fullscreen mode Exit fullscreen mode

Rust vs Java (Tabular comparison)

Top comments (0)