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

  1. 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 a Box<dyn DataSource>.
  1. Database and File Structs: These are two types of data sources that implement DataSource.
  • Database‘s get_backup_source returns a File as a backup.
  • File‘s get_backup_source returns a Database as a backup.
  1. Dynamic Dispatch in Action:
  • The main function initializes primary_source as a Database.
  • It then reads data from the primary source.
  • get_backup_source returns the backup source dynamically, enabling seamless switching between Database and File 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:

  1. The same concrete type is returned: For example, if get_backup_source is called on a Database, the concrete type inside the Box<dyn DataSource> would still be Database.
  2. The type is stored as a trait object: By using Box<dyn DataSource>, you’re wrapping the Database or File instance in a way that treats it as a DataSource trait object, which is why Rust lets you use dynamic dispatch.
  3. You get ownership of the instance: Box owns the contained data, so if you return Box<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.

Rust Programming

Previous article

map_err challenge