Rust – Trait Object – Dynamic Dispatch Example

Let’s consider a more real-world example. Imagine you have a system that represents different types of data sources, like Database and File, each implementing a DataSource trait. The DataSource trait has a method get_backup_source that returns a backup data source as a trait object. This could be useful if your application needs to switch to a backup source dynamically, say from a primary database to a secondary file-based backup.

This example shows how to create interchangeable primary and backup sources like databases and file storage, with each source implementing a shared DataSource trait.
Example Code
By using Box<dyn DataSource>, you’re actually creating a trait object, which allows you to store the type implementing DataSource inside a Box. This abstraction enables you to return a value of the same concrete type (like Database or File) without explicitly specifying it, effectively hiding the underlying type behind the trait.
use std::fmt::Debug;
// Trait for any data source
trait DataSource: Debug {
fn read_data(&self) -> String;
fn get_backup_source(&self) -> Box<dyn DataSource>;
}
// A struct representing a Database source
#[derive(Debug)]
struct Database {
name: String,
}
impl DataSource for Database {
fn read_data(&self) -> String {
format!("Reading data from database: {}", self.name)
}
fn get_backup_source(&self) -> Box<dyn DataSource> {
Box::new(File { path: "/backup/file_backup.txt".into() })
}
}
// A struct representing a File source
#[derive(Debug)]
struct File {
path: String,
}
impl DataSource for File {
fn read_data(&self) -> String {
format!("Reading data from file: {}", self.path)
}
fn get_backup_source(&self) -> Box<dyn DataSource> {
Box::new(Database { name: "backup_db".into() })
}
}
fn main() {
// Primary data source as a Database
let primary_source: Box<dyn DataSource> = Box::new(Database { name: "main_db".into() });
// Read from primary source
println!("{}", primary_source.read_data());
// Get a backup source and read from it
let backup_source = primary_source.get_backup_source();
println!("{}", backup_source.read_data());
}
Explanation
DataSourceTrait: Defines methods that all data sources must implement:
read_data: A method to read data, returning a string representation.get_backup_source: Returns a backup data source as aBox<dyn DataSource>.
DatabaseandFileStructs: These are two types of data sources that implementDataSource.
Database‘sget_backup_sourcereturns aFileas a backup.File‘sget_backup_sourcereturns aDatabaseas a backup.
- Dynamic Dispatch in Action:
- The
mainfunction initializesprimary_sourceas aDatabase. - It then reads data from the primary source.
get_backup_sourcereturns the backup source dynamically, enabling seamless switching betweenDatabaseandFiletypes at runtime.
This approach is useful for scenarios where different types of data sources are interchangeable and can failover to backups without knowing the specific backup type at compile time.
So, in practice:
- The same concrete type is returned: For example, if
get_backup_sourceis called on aDatabase, the concrete type inside theBox<dyn DataSource>would still beDatabase. - The type is stored as a trait object: By using
Box<dyn DataSource>, you’re wrapping theDatabaseorFileinstance in a way that treats it as aDataSourcetrait object, which is why Rust lets you use dynamic dispatch. - You get ownership of the instance:
Boxowns the contained data, so if you returnBox<dyn DataSource>, the calling code has exclusive ownership of that boxed instance.
In summary, Box<dyn DataSource> allows returning the same type (e.g., Database or File), but wrapped in a Box as a trait object. This abstraction enables polymorphism without needing to know the concrete type.

