RustArena

Step 7 of 7

Downcasting with Any (briefly)

Once a value is behind dyn Trait, its concrete type is forgotten. Occasionally you need it back. The standard trait std::any::Any makes that possible: every type without borrowed references implements it automatically, and a &dyn Any can be asked "are you a T?" with downcast_ref::<T>(), which returns Some(&T) or None.

use std::any::Any;

fn describe(value: &dyn Any) -> String {
    if let Some(n) = value.downcast_ref::<i32>() {
        format!("an i32: {n}")
    } else if let Some(s) = value.downcast_ref::<String>() {
        format!("a String: {s}")
    } else {
        String::from("something else")
    }
}

fn main() {
    let values: Vec<Box<dyn Any>> = vec![Box::new(42), Box::new(String::from("crab")), Box::new(1.5)];
    for v in &values {
        println!("{}", describe(v.as_ref()));
    }
}
an i32: 42
a String: crab
something else

With your own trait

To downcast your own trait objects, make Any a supertrait: trait Event: Any means "every Event is also an Any" (the advanced traits module covers supertraits). Since Rust 1.86, a &dyn Event then converts to a &dyn Any directly (trait upcasting):

use std::any::Any;

trait Event: Any {
    fn name(&self) -> String;
}

struct Login {
    user: String,
}

struct Logout;

impl Event for Login {
    fn name(&self) -> String {
        String::from("login")
    }
}

impl Event for Logout {
    fn name(&self) -> String {
        String::from("logout")
    }
}

fn main() {
    let events: Vec<Box<dyn Event>> = vec![Box::new(Login { user: String::from("ana") }), Box::new(Logout)];
    for e in &events {
        let any: &dyn Any = e.as_ref();
        match any.downcast_ref::<Login>() {
            Some(login) => println!("{} by {}", e.name(), login.user),
            None => println!("{}", e.name()),
        }
    }
}
login by ana
logout

Older code does the same with an extra trait method, fn as_any(&self) -> &dyn Any { self }, in every impl. You'll still see it in many codebases.

Use it rarely

Downcasting gives up what trait objects are for: if code keeps asking "which type are you really?", the trait is probably missing a method, or an enum would fit better (previous step). Legitimate uses exist: error handling (Box<dyn Error> and anyhow::Error have downcast_ref to check for a specific error type), plugin systems, and type-keyed maps in game engines and web frameworks.

Common mistakes

Your task

A config loader produced a list of mixed values. Write fn sum_numbers(values: &[Box<dyn Any>]) -> i64, which adds up every i64 and every i32 (converted to i64) and ignores everything else, and fn count_text(values: &[Box<dyn Any>]) -> usize, which counts the Strings and &'static strs.

Challenge 2

Fill in the blank to ask the value whether it's an i32. The program prints got 7 and then not an i32.

Challenge 3

An audit log stores events as Box<dyn Event>. Write fn logins(events: &[Box<dyn Event>]) -> Vec<String> that returns the user of every Login event, in order, by downcasting (upcast each event to &dyn Any first, as in the example). Also implement Event for Purchase (name purchase).

Previous: Enums or trait objects?