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
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();
}
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
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();
}
}
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
Top comments (0)