Trait-object upcast was recently merged. Unfortunately downcasting is still not very user-friendly.
Motivation
Given
use std::{any::Any, ops::Deref};
trait A: Any {}
trait B: A {}
impl A for () {}
impl B for () {}
we can now easily upcast trait objects:
let b = Box::new(()) as Box<dyn B>;
let a = b as Box<dyn A>; // upcast
Type-checking and downcast is supported, but usage is less clear:
assert!((a.deref() as &dyn Any).is::<()>());
let c = (a as Box<dyn Any>).downcast::<()>();
We should be able to improve this to something like the following:
assert!(a.inner_type_is::<()>()); // method defined on Box
assert!(a.deref().type_is::<()>()); // alternative
// Note that we type-check the contents, not the Box!
let c = a.downcast::<()>();
Implementating details
Type-checking
std::any::Any
plus TypeId
provide all we need, but the following is not user-friendly:
assert_eq!(a.deref().type_id(), TypeId::of::<()>());
Currently, fn
is
is defined on dyn Any
, dyn Any + Send
and dyn Any + Send + Sync
. This implies that (1) it is not directly available on dyn A
, dyn B
etc. and (2) it is not available on generics like T: Any
.
Aside: is
is too short/generic a name for a widely available method, hence I use the longer name type_is
below.
Options for making type_is
more accessible:
-
As an additional method on
Any
:fn type_is<U: Any>(&self) -> bool;
Usage on a
Box
requires an explicit dereference to test the inner type, not the type of theBox
:a.deref().type_is::<()>()
or(*a).type_is::<()>()
.Issue: this is technically a breaking change since anywhere that
Any
is in scope, all deriving trait objects will gain a new method. This could in theory conflict with user-defined methods on derived traits. Likely impact: low. -
The same method, but on an extension trait
AnyExt: Any
.This would be provided with
impl<T: Any + ?Sized> AnyExt for T {..}
. Users must explicitly importstd::any::AnyExt
, thus avoiding unexpected conflicts. -
A similar method placed directly on
Box
(and similar container types) which emphasizes that it tests the inner type:fn inner_type_is<U: Any>(&self) -> bool;
This approach is more user-friendly than (1) and (2) since there is no need to
deref()
(nor the footgun of accidentally forgetting), while still covering likely use-cases.Like (1) there is potential breakage, but with much lower expected impact (more restricted availability and more complex name), thus this should be a non-issue.
Downcast
Downcast is necessarily implemented on containing types like Box
. Today, Box
provides impls for three cases:
Box<dyn Any>
Box<dyn Any + Send>
Box<dyn Any + Send + Sync
This does not cover user-defined traits like Box<dyn B>
.
What we really want is to define downcast
on Box<T>
where T: Any + ?Sized
, but for obscure reasons some types like str
and [i32]
technically implement Any
(unsafely due to length being omitted from the TypeId
) while not supporting the ability to type-cast to dyn Any
.
What we need to make the generics work is provided by Unsize
:
pub trait BoxExt: Sized {
/// Optional method: type-check inner value
fn inner_type_is<U: Any>(&self) -> bool;
/// Attempt to downcast the inner value
fn downcast<U: Any>(self) -> Result<Box<U>, Self>;
}
impl<T: Any + Unsize<dyn Any> + ?Sized> BoxExt for Box<T> {
fn inner_type_is<U: Any>(&self) -> bool {
self.deref().type_is::<U>()
}
fn downcast<U: Any>(self) -> Result<Box<U>, Self> {
if self.inner_type_is::<U>() {
unsafe {
// This cast requires T: Unsize<dyn Any> :
let any: Box<dyn Any> = self as Box<dyn Any>;
let raw: *mut dyn Any = Box::into_raw(any);
Ok(Box::from_raw(raw as *mut U))
}
} else {
Err(self)
}
}
}
Full example on the playground
Usage on non-dyn types
Since the above are generic, they work on non-dyn
types:
let b = Box::new(());
assert!(b.inner_type_is::<()>());
let b: Box<()> = b.downcast().unwrap();
This is fine, and has some utility in generic code.
Introducing the above
The first potential blocker is that Unsize
has not yet been stabilized. It is however already widely used within the std
library so this may not be an issue.
The second issue potential issue is breaking changes. It is possible to implement user-defined methods on Box
via extension traits as demonstrated above but not inherent methods. While such conflicts cannot be ruled out, they are unlikely.
Proposal
Implement the following on Box
, replacing the current impls of downcast
:
impl<T: Any + Unsize<dyn Any> + ?Sized, A: Allocator> Box<T, A> {
/// Type-check inner value
pub fn inner_type_is<U: Any>(&self) -> bool {
self.deref().type_id() == TypeId::of::<U>()
}
/// Attempt to downcast the inner value
pub fn downcast<U: Any>(self) -> Result<Box<U, A>, Self> {
if self.inner_type_is::<U>() {
unsafe {
let raw: *mut dyn Any = Box::into_raw(self as Box<dyn Any, A>);
Ok(Box::from_raw(raw as *mut U))
}
} else {
Err(self)
}
}
}
Do the same for other container types currently providing fn downcast
(Error
, Rc
, Arc
).
The method fn downcast
is strict superset of the existing downcast
methods on Box
. There is some potential for collision through user-defined extension trait on Box
(certainly worth a crater run).
The method fn inner_type_is
is an optional extra with some applications such as passing messages via a variadic queue/stack (where only messages of known type are removed).
Alternative: implement via extension traits
Instead of proposal 1, introduce the same methods via an extension trait BoxExt
(respectively ErrorExt
, RcExt
, ArcExt
) which requires explicit import. Keep the existing downcast
methods (not a conflict since inherent methods are preferred over trait-provided methods).
This avoids the (low) potential for breakage through name conflicts but has worse usability: discovery, API complexity and need-to-import.