Dynamic Typing
Determine the Type of an Object at Runtime and Downcast with the Any
Trait
Dynamic typing in Rust is limited compared to other languages, but the std::any
↗ module provides ways to obtain type information at runtime.
std::any
is primarily used in scenarios where you need to store and retrieve data of various, unknown concrete types within a collection or context that uses trait objects (specifically dyn Any
). This often comes up in:
- Plugins: If you're building a system where users can register custom data or components, and you need to store them polymorphically,
Any
↗ allows you to later retrieve them and process them based on their original concrete type. - Generic Event Systems: In an event system where different types of events might be processed by a single handler,
Any
can help identify and process specific event types. - Reflection-like capabilities: While Rust doesn't have full reflection like some other languages,
Any
provides a limited form of runtime type inspection.
The TypeId
↗ struct represents a globally unique, opaque identifier for a type, allowing you to compare types without needing to know their names at compile time. A TypeId
is currently only available for types which are 'static
.
You can use TypeId
to check if two types are the same or as a key to store type information in e.g. a HashMap
:
use std::any::Any; use std::any::TypeId; // `of` returns the `TypeId` of the generic type parameter. // // The `'static` bound below is necessary, // because `TypeId` only works with `'static` types. fn is_same_type<T: 'static, U: 'static>() -> bool { TypeId::of::<T>() == TypeId::of::<U>() } fn main() { // Compare types with `TypeId::of`:` println!("i32 vs i32: {}", is_same_type::<i32, i32>()); // true. println!("i32 vs u32: {}", is_same_type::<i32, u32>()); // false. // The `Any` trait can be used to get a `TypeId`: let value: &dyn Any = &"hello"; let tid = value.type_id(); // `TypeId` implements `Debug` but its value is just an opaque number. // See below to get a type name. println!("TypeId of value: {tid:?}"); // However, given a smart pointer containing `dyn Any`, // calling `.type_id()` on the smart pointer will produce the `TypeId` of // the container, not the underlying object: let boxed: Box<dyn Any> = Box::new(3_i32); #[allow(clippy::type_id_on_box)] let boxed_id = boxed.type_id(); // To get the `TypeId` of the inner value, // use `*` to first dereference the `Box` (calling `Deref`). let actual_id = (*boxed).type_id(); assert_eq!(actual_id, TypeId::of::<i32>()); assert_eq!(boxed_id, TypeId::of::<Box<dyn Any>>()); // To get a type _name_ for diagnostics purposes, use `std::any::type_name` // or `type_name_of_val`. Note: the returned name may vary with compiler // versions. println!("{}", std::any::type_name::<String>()); }
The std::any::Any
↗ trait is the core of the std::any
module. It is a special built-in trait that determines the concrete type of a trait object at runtime and provides the ability to downcast a trait object to a concrete type, when the concrete type is known ("downcasting" converts a reference of a general type (here dyn Any
and related types) to a reference of a more specific type, if the value is of that type). The Any
trait provides a few methods:
use std::any::Any; // - `is::<T>(&self) -> bool` checks if the underlying concrete type of the // trait object is `T`. // - `downcast_ref::<T>(&self) -> Option<&T>` attempts to downcast the trait // object to a reference `&T`. // It returns `Some(&T)` if successful, `None` otherwise. // - `downcast_mut::<T>(&mut self) -> Option<&mut T>` is similar to // `downcast_ref`, but provides a mutable reference. // // All are implemented on `dyn Any`, `dyn Any + Send`, `dyn Any + Send + Sync`. fn is_string(s: &dyn Any) -> bool { // `is<T>` is equivalent to `TypeId::of::<T>() == s.type_id()`. let res = s.is::<String>(); if res { print!("String: "); } else { println!("Not a string..."); } // Get access to the `&String` if `s` is of the right type: if let Some(string) = s.downcast_ref::<String>() { println!("{string}"); } res } // `downcast::<T>(self: Box<Self>) -> Result<Box<T>, Box<Self>>` consumes a // `Box<dyn Any>` and attempts to downcast it to a `Box<T>`. `downcast` is // implemented on `Box<dyn Any>`, `Box<dyn Any + Send>`, `Box<dyn Any + Send + // Sync>`. fn print_if_string(value: Box<dyn Any>) { if let Ok(string) = value.downcast::<String>() { println!("String: {string}"); } } fn main() { assert!(!is_string(&42)); let s = "a string".to_string(); assert!(is_string(&s)); print_if_string(Box::new(s)); }
Note a few caveats with Any
:
- The
Any
trait can only be used with types that have a'static
lifetime. This means they must not contain any non-'static
references. - Runtime Overhead: Downcasting involves runtime checks (
is::<T>
↗). While not excessively slow, it's less performant than static dispatch. - Loss of Compile-Time Guarantees: When you put something into
Box<dyn Any>
or similar, you lose type information at compile time. This makes it harder for the compiler to catch errors for you. - If there's a way to achieve your goal using generics with trait bounds, enums, or other more type-safe Rust patterns, those are generally preferred over
Any
.Any
should be used when you genuinely need runtime type introspection for heterogeneous collections.
Related Topics
- Development Tools: Cargo Plugins.
- Scripting.
- Traits.