Downcasting Traits in Rust

Ben Ashby · January 11, 2025

Introduction

Downcasting in Rust is not as straightforward as it might be in some other languages (like Java) where you can simply cast an object to a subclass type. Rust’s emphasis on safety and explicit type handling means that casting to a concrete type requires a bit more work. In this post, we’ll explore why you might want to downcast, when it might (or might not) be appropriate, and the most idiomatic way to do it in Rust.

Why Might You Need Downcasting?

A common scenario where downcasting comes up is when you have a collection or other data structure holding multiple types behind the same trait. For example:

let mut speakers: Vec<Box<dyn Speaker>> = vec![];
speakers.push(Box::new(Dog));
speakers.push(Box::new(Cat));
// ...

At some point, you might need to access a type-specific method or behavior that the trait itself does not expose. In many languages, you could do something like a ((Dog) speaker).bark(). In Rust, traits don’t automatically allow you to “downcast” to the original concrete type. Instead, you need to opt in to certain facilities provided by the standard library.

That said, it’s worth noting that downcasting is sometimes a design smell—if you rely on it often, you might want to explore other patterns like generics or enums, which allow more compile-time checking and clear type dispatch. Downcasting is more of a “last resort” when these more idiomatic Rust approaches don’t fit your problem.

The Problem

To illustrate, let’s define a trait Speaker and two structs Dog and Cat that implement this trait:

trait Speaker {
    fn speak(&self);
}

struct Dog;

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

struct Cat;

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

You might expect to be able to do something like this:

fn broken_downcasting_example() {
    let dog = Dog;
    dog.speak();

    let speaker: &dyn Speaker = &dog;
    speaker.speak();

    // Attempting to downcast to `Dog`:
    if let Some(dog) = speaker.downcast_ref::<Dog>() {
        dog.speak();
    } else {
        println!("This is not a dog!");
    }
}

However, if you try to compile this code, you’ll get an error:

error[E0599]: no method named `downcast_ref` found for reference `&dyn Speaker` in the current scope
  --> src\traits\downcasting_traits.rs:30:32
   |
30 |     if let Some(dog) = speaker.downcast_ref::<Dog>() {
   |                                ^^^^^^^^^^^^ method not found in `&dyn Speaker`

Explanation: Under the Hood

What’s happening here is that Rust does not provide a built-in downcast_ref method on arbitrary trait objects like &dyn Speaker. This method is provided by the std::any::Any trait (via downcast_ref and downcast_mut), but your trait must explicitly opt in to Any to gain that functionality.

Even if you try something like this:

use std::any::Any;

trait Speaker: Any {
    fn speak(&self);
}

Rust still won’t let you just call downcast_ref on &dyn Speaker because the compiler must have a guarantee that every implementor of Speaker can also be treated as Any. Moreover, you need a way to “unwrap” the trait object into an Any. This is where the common “as_any trick” comes in.

The Idiomatic Way: Using as_any

A widely accepted solution is to provide a helper method that returns &dyn Any from your trait:

use std::any::Any;

trait Speaker {
    fn speak(&self);
    fn as_any(&self) -> &dyn Any;
}

This way, you can obtain a reference to the underlying Any, after which you can perform the downcast. Each implementor of Speaker will need to supply an as_any implementation:

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

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

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

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

Now, you can safely downcast:

fn downcasting_example() {
    let dog = Dog;
    dog.speak();

    let speaker: &dyn Speaker = &dog;
    speaker.speak();

    // Using `as_any` to get a `&dyn Any`, then calling `downcast_ref::<Dog>()`
    if let Some(dog) = speaker.as_any().downcast_ref::<Dog>() {
        dog.speak();
    } else {
        println!("This is not a dog!");
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_downcasting() {
        downcasting_example();
    }
}

With this approach, Rust knows how to treat the trait object as Any, and you get the methods required (downcast_ref, downcast_mut, etc.) to attempt a downcast at runtime.

When Not to Downcast

While it can be quite handy in certain scenarios, you should also be aware that:

  1. Downcasting can obscure code: It sometimes breaks the abstraction that traits provide and makes code harder to follow.
  2. Other patterns might be better: Generics, enums (with variants for each concrete type), or pattern matching can often replace the need for downcasting. These are usually more idiomatic in Rust and provide more compile-time guarantees.
  3. Slight runtime cost: Using Any is typically quite efficient, but there is still a small runtime cost compared to a purely static dispatch with generics or a sum type (enum).

Additional Considerations

  1. Mutable references and owned trait objects: You can also downcast mutable trait objects with downcast_mut, or if you have Box<dyn Any>, you can attempt a downcast with Box<dyn Any>::downcast::<T>().
  2. Community crates: If your needs go beyond these basics, consider looking at crates like downcast-rs which provide more ergonomic patterns for downcasting.
  3. File structure: If you’re new to Rust, remember to show your full file layout and Cargo.toml if you rely on external crates. For the standard library features (std::any::Any), no extra dependencies are needed.

Conclusion

Downcasting in Rust requires some additional boilerplate because the language is explicit about ensuring type safety at compile time. By implementing an as_any method in your trait, you can opt in to runtime type checks and safely cast back to a concrete type.

However, keep in mind that downcasting should be used judiciously. If you’re frequently needing it, consider whether Rust’s powerful enums, generics, or other design patterns might offer a more idiomatic and maintainable solution.

Happy coding and downcasting!

Twitter, Facebook