Item 19: Avoid reflection
Programmers coming to Rust from other languages are often used to reaching for reflection as a tool in their toolbox. They can waste a lot of time trying to implement reflection-based designs in Rust, only to discover that what they're attempting can only be done poorly, if at all. This Item hopes to save that time wasted exploring dead ends, by describing what Rust does and doesn't have in the way of reflection, and what can be used instead.
Reflection is the ability of a program to examine itself at runtime. Given an item at runtime, it covers these questions:
- What information can be determined about the item's type?
- What can be done with that information?
Programming languages with full reflection support have extensive answers to these questions. Languages with reflection typically support some or all of the following at runtime, based on the reflection information:
- Determining an item's type
- Exploring its contents
- Modifying its fields
- Invoking its methods
Languages that have this level of reflection support also tend to be dynamically typed languages (e.g., Python, Ruby), but there are also some notable statically typed languages that also support reflection, particularly Java and Go.
Rust does not support this type of reflection, which makes the advice to avoid reflection easy to follow at this level—it's just not possible. For programmers coming from languages with support for full reflection, this absence may seem like a significant gap at first, but Rust's other features provide alternative ways of solving many of the same problems.
C++ has a more limited form of reflection, known as run-time type identification (RTTI). The
typeid
operator returns a unique identifier
for every type, for objects of polymorphic type (roughly: classes with virtual functions):
typeid
: Can recover the concrete class of an object referred to via a base class referencedynamic_cast<T>
: Allows base class references to be converted to derived classes, when it is safe and correct to do so
Rust does not support this RTTI style of reflection either, continuing the theme that the advice of this Item is easy to follow.
Rust does support some features that provide similar functionality in the
std::any
module, but they're limited (in ways we will explore) and so
best avoided unless no other alternatives are possible.
The first reflection-like feature from std::any
looks like magic at first—a way of determining the name of an
item's type. The following example uses a user-defined tname()
function:
let x = 42u32;
let y = vec![3, 4, 2];
println!("x: {} = {}", tname(&x), x);
println!("y: {} = {:?}", tname(&y), y);
to show types alongside values:
x: u32 = 42
y: alloc::vec::Vec<i32> = [3, 4, 2]
The implementation of tname()
reveals what's up the compiler's sleeve: the function is generic
(as per Item 12), and so each invocation of it is actually a different function (tname::<u32>
or tname::<Square>
):
#![allow(unused)] fn main() { fn tname<T: ?Sized>(_v: &T) -> &'static str { std::any::type_name::<T>() } }
The implementation is provided by the
std::any::type_name<T>
library
function, which is also generic. This function has access only to compile-time information; there is no code run that
determines the type at runtime. Returning to the trait object types used in Item 12 demonstrates this:
let square = Square::new(1, 2, 2);
let draw: &dyn Draw = □
let shape: &dyn Shape = □
println!("square: {}", tname(&square));
println!("shape: {}", tname(&shape));
println!("draw: {}", tname(&draw));
Only the types of the trait objects are available, not the type (Square
) of the concrete underlying item:
square: reflection::Square
shape: &dyn reflection::Shape
draw: &dyn reflection::Draw
The string returned by type_name
is suitable only for diagnostics—it's explicitly a "best-effort" helper whose
contents may change and may not be unique—so don't attempt to parse type_name
results. If you
need a globally unique type identifier, use TypeId
instead:
#![allow(unused)] fn main() { use std::any::TypeId; fn type_id<T: 'static + ?Sized>(_v: &T) -> TypeId { TypeId::of::<T>() } }
println!("x has {:?}", type_id(&x));
println!("y has {:?}", type_id(&y));
x has TypeId { t: 18349839772473174998 }
y has TypeId { t: 2366424454607613595 }
The output is less helpful for humans, but the guarantee of uniqueness means that the result can be used in code.
However, it's usually best not to use TypeId
directly but to use the
std::any::Any
trait instead, because the standard
library has additional functionality for working with Any
instances (described below).
The Any
trait has a single method type_id()
,
which returns the TypeId
value for the type that implements the trait. You can't implement this trait yourself, though,
because Any
already comes with a blanket implementation for most arbitrary types T
:
impl<T: 'static + ?Sized> Any for T {
fn type_id(&self) -> TypeId {
TypeId::of::<T>()
}
}
The blanket implementation doesn't cover every type T
: the T: 'static
lifetime bound means that if T
includes any references that have a non-'static
lifetime, then TypeId
is not implemented for T
. This is a
deliberate restriction that's imposed because lifetimes aren't fully
part of the type: TypeId::of::<&'a T>
would be the same as TypeId::of::<&'b T>
, despite the differing lifetimes,
increasing the likelihood of confusion and unsound code.
Recall from Item 8 that a trait object is a fat pointer that holds a pointer to the underlying item,
together with a pointer to the trait implementation's vtable. For Any
, the vtable has a single entry, for a
type_id()
method that returns the item's type, as shown in Figure 3-4:
let x_any: Box<dyn Any> = Box::new(42u64);
let y_any: Box<dyn Any> = Box::new(Square::new(3, 4, 3));
Aside from a couple of indirections, a dyn Any
trait object is effectively a combination of a raw pointer and a type
identifier. This means that the standard library can offer some additional generic methods that are defined for a
dyn Any
trait object; these methods are generic over some additional type T
:
is::<T>()
: Indicates whether the trait object's type is equal to some specific other typeT
downcast_ref::<T>()
: Returns a reference to the concrete typeT
, provided that the trait object's type matchesT
downcast_mut::<T>()
: Returns a mutable reference to the concrete typeT
, provided that the trait object's type matchesT
Observe that the Any
trait is only approximating reflection functionality: the programmer chooses (at compile time) to
explicitly build something (&dyn Any
) that keeps track of an item's compile-time type as well as its location. The
ability to (say) downcast back to the original type is possible only if the overhead of building an Any
trait object
has already happened.
There are comparatively few scenarios where Rust has different compile-time and runtime types associated with an item.
Chief among these is trait objects: an item of a concrete type Square
can be coerced into a trait
object dyn Shape
for a trait that the type implements. This coercion builds a fat pointer (object + vtable) from a
simple pointer (object/item).
Recall also from Item 12 that Rust's trait objects are not really object-oriented. It's not the case that a
Square
is-a Shape
; it's just that a Square
implements Shape
's interface. The same is true for trait
bounds: a trait bound Shape: Draw
does not mean is-a; it just means
also-implements because the vtable for Shape
includes the entries for the methods of Draw
.
For some simple trait bounds:
trait Draw: Debug {
fn bounds(&self) -> Bounds;
}
trait Shape: Draw {
fn render_in(&self, bounds: Bounds);
fn render(&self) {
self.render_in(overlap(SCREEN_BOUNDS, self.bounds()));
}
}
the equivalent trait objects:
let square = Square::new(1, 2, 2);
let draw: &dyn Draw = □
let shape: &dyn Shape = □
have a layout with arrows (shown in Figure 3-5; repeated from Item 12) that make the problem clear: given a dyn Shape
object, there's no immediate way to build a dyn Draw
trait object, because there's no way to get back to the
vtable for impl Draw for Square
—even though the relevant part of its contents (the address of the
Square::bounds()
method) is theoretically recoverable. (This is likely to change in later versions of Rust; see the
final section of this Item.)
Comparing this with the previous diagram, it's also clear that an explicitly constructed &dyn Any
trait object doesn't help.
Any
allows recovery of the original concrete type of the underlying item, but there is no runtime way to
see what traits it implements, or to get access to the relevant vtable that might allow creation of a trait object.
So what's available instead?
The primary tool to reach for is trait definitions, and this is in line with advice for other languages—Effective Java Item 65 recommends, "Prefer interfaces to reflection". If code needs to rely on the availability of certain behavior for an item, encode that behavior as a trait (Item 2). Even if the desired behavior can't be expressed as a set of method signatures, use marker traits to indicate compliance with the desired behavior—it's safer and more efficient than (say) introspecting the name of a class to check for a particular prefix.
Code that expects trait objects can also be used with objects having backing code that was not available at program link time,
because it has been dynamically loaded at runtime (via dlopen(3)
or equivalent)—which means that
monomorphization of a generic (Item 12) isn't possible.
Relatedly, reflection is sometimes also used in other languages to allow multiple incompatible versions of the same dependency library to be loaded into the program at once, bypassing linkage constraints that There Can Be Only One. This is not needed in Rust, where Cargo already copes with multiple versions of the same library (Item 25).
Finally, macros—especially derive
macros—can be used to auto-generate
ancillary code that understands an item's type at compile time, as a more efficient and more type-safe equivalent to
code that parses an item's contents at runtime. Item 28 discusses Rust's macro system.
Upcasting in Future Versions of Rust
The text of this Item was first written in 2021, and remained accurate all the way until the book was being prepared for publication in 2024—at which point a new feature is due to be added to Rust that changes some of the details.
This new "trait upcasting" feature enables upcasts that
convert a trait object dyn T
to a trait object dyn U
, when U
is one of T
's supertraits (trait T: U {...}
).
The feature is gated on #![feature(trait_upcasting)]
in advance of its official release, expected to be Rust version
1.76.
For the preceding example, that means a &dyn Shape
trait object can now be converted to a &dyn Draw
trait object,
edging closer to the is-a relationship of Liskov
substitution. Allowing this conversion has a knock-on
effect on the internal details of the vtable implementation, which are likely to become more complex than the versions
shown in the preceding diagrams.
However, the central points of this Item are not affected—the Any
trait has no supertraits, so the ability to
upcast adds nothing to its functionality.