Rust - Programming Language

Downcasting

When do you need downcasting?

Requirements for Downcasting

  1. The trait must be 'static (no non-static lifetimes).
  2. The trait must inherit from the Any trait.

Example: Downcasting with Any

use std::any::Any;

trait Animal: Any {
    fn speak(&self);
    
    // This allows using `as_any()` for downcasting.
    fn as_any(&self) -> &dyn Any;
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl Animal for Cat {
    fn speak(&self) {
        println!("Meow!");
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];

    for animal in animals.iter() {
        animal.speak();

        // Try downcasting
        if let Some(dog) = animal.as_any<Dog>( {
            println!("This is a dog!");
        } else if let Some(cat) = animal.as_any<Cat>( {
            println!("This is a cat!");
        }
    }
}

Explanation:

Limitations

Summary

Term Meaning
Trait Object A dynamically-dispatched type like dyn Trait
Upcasting Concrete type → Trait object
Downcasting Trait object → Concrete type
Any Trait Enables runtime type inspection & downcasting
downcast_ref Try to get a reference to the original type

Patterns and Idioms

Builder Pattern

Core Concept

The Builder Pattern is a design pattern used to construct complex objects step-by-step. Instead of using a single, complicated constructor with many parameters, you use a separate Builder object to configure the final object's properties before creating it.

This pattern solves two common problems:

  1. The "Telescoping Constructor": Avoids having multiple constructors or a single constructor with a long, confusing list of parameters (e.g., new(arg1, arg2, None, true, ...)).
  2. Lack of Safety: Avoids creating an object with public fields that can be set in an invalid state (e.g., creating an object and forgetting to set a required field).
    The process involves three parts:

Example

Here's how to build a ServerConfig object that requires a host and port but has an optional timeout.

Implementation

// The final, immutable object. Its fields are private.
pub struct ServerConfig {
    host: String,
    port: u16,
    timeout: u64, // has a default value
}

// The builder struct that holds the configuration.
pub struct ServerConfigBuilder {
    host: String,
    port: u16,
    timeout: Option<u64>,
}

impl ServerConfigBuilder {
    // 1. Start with the required parameters.
    pub fn new(host: String, port: u16) -> Self {
        Self {
            host,
            port,
            timeout: None,
        }
    }

    // 2. Add methods for optional parameters. This is a "fluent" interface.
    pub fn timeout(mut self, timeout_ms: u64) -> Self {
        self.timeout = Some(timeout_ms);
        self
    }

    // 3. The build method consumes the builder and creates the final object.
    // All validation and default value logic lives here.
    pub fn build(self) -> ServerConfig {
        ServerConfig {
            host: self.host,
            port: self.port,
            // Use the configured timeout or a default value.
            timeout: self.timeout.unwrap_or(5000), 
        }
    }
}

Usage

// Create a simple configuration using defaults
let basic_config = ServerConfigBuilder::new("localhost".to_string(), 8080)
    .build();

// Create a more complex configuration by chaining methods
let custom_config = ServerConfigBuilder::new("1.1.1.1".to_string(), 443)
    .timeout(10_000)
    .build();

The Builder Pattern

Core Concept

The Builder Pattern is a design pattern used to construct complex objects step-by-step. Instead of using a single, complicated constructor with many parameters, you use a separate Builder object to configure the final object's properties before creating it.

This pattern solves two common problems:

  1. The "Telescoping Constructor": Avoids having multiple constructors or a single constructor with a long, confusing list of parameters (e.g., new(arg1, arg2, None, true, ...)).
  2. Lack of Safety: Avoids creating an object with public fields that can be set in an invalid state (e.g., creating an object and forgetting to set a required field).

The process involves three parts:

Example in Rust

Here's how to build a ServerConfig object that requires a host and port but has an optional timeout.

Rust Implementation

// The final, immutable object. Its fields are private.
pub struct ServerConfig {
    host: String,
    port: u16,
    timeout: u64, // has a default value
}

// The builder struct that holds the configuration.
pub struct ServerConfigBuilder {
    host: String,
    port: u16,
    timeout: Option<u64>,
}

impl ServerConfigBuilder {
    // 1. Start with the required parameters.
    pub fn new(host: String, port: u16) -> Self {
        Self {
            host,
            port,
            timeout: None,
        }
    }

    // 2. Add methods for optional parameters. This is a "fluent" interface.
    pub fn timeout(mut self, timeout_ms: u64) -> Self {
        self.timeout = Some(timeout_ms);
        self
    }

    // 3. The build method consumes the builder and creates the final object.
    // All validation and default value logic lives here.
    pub fn build(self) -> ServerConfig {
        ServerConfig {
            host: self.host,
            port: self.port,
            // Use the configured timeout or a default value.
            timeout: self.timeout.unwrap_or(5000), 
        }
    }
}

Usage
Rust

// Create a simple configuration using defaults
let basic_config = ServerConfigBuilder::new("localhost".to_string(), 8080)
    .build();

// Create a more complex configuration by chaining methods
let custom_config = ServerConfigBuilder::new("1.1.1.1".to_string(), 443)
    .timeout(10_000)
    .build();

Advantages and Disadvantages