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
DataSource
Trait: 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>
.
Database
andFile
Structs: These are two types of data sources that implementDataSource
.
Database
‘sget_backup_source
returns aFile
as a backup.File
‘sget_backup_source
returns aDatabase
as a backup.
- Dynamic Dispatch in Action:
- The
main
function initializesprimary_source
as aDatabase
. - It then reads data from the primary source.
get_backup_source
returns the backup source dynamically, enabling seamless switching betweenDatabase
andFile
types 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_source
is 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 theDatabase
orFile
instance in a way that treats it as aDataSource
trait object, which is why Rust lets you use dynamic dispatch. - You get ownership of the instance:
Box
owns 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.