4.8 dyn Any

译文 · 基于 Learning Rust

dyn Any

原文链接: https://quinedot.github.io/rust-learning/dyn-any.html

我们一再强调 dyn Trait 不是超类型也不是动态类型的一种形式,但在其中一个示例中我们看到 dyn Any 可以「向下转型」回擦除的基类型。或者引用官方文档: Any 是

用于模拟动态类型的 trait。

此处我们专门更仔细地审视 dyn Any。

基本思路

Any trait 为所有满足 'static 约束的类型实现。它提供 type_id 方法,返回实现类型的不透明但唯一的标识符。我们还有 TypeId::of::<T>, 可查找任何 'static 类型的 TypeId。

结合起来,可通过类似以下的操作 进行可失败的向下转型:

1
2
3
4
5
6
7
8
9
    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
        if self.is::<T>() {
            // SAFETY: 刚检查了是否指向正确类型,可依赖该检查保证内存安全,
            // 因为我们为所有类型实现了 Any;不存在其他 impl,否则会与我们的 impl 冲突。
            unsafe { Some(self.downcast_ref_unchecked()) }
        } else {
            None
        }
    }

而 is 只是将 TypeId::of::<T>() 与 self.type_id() 比较。

具体向下转型的细节撇开,大致就是这样!所需的无非是全局标识符(TypeId)。标准库提供各种向下转型方法以封装所需的 unsafe。

如前提醒, vtable 指针本身不适合作为擦除类型的全局标识符。同一 trait 可能因代码生成单元和链接器实现而有多个 vtable,不同 trait 可能因去重优化而有相同 vtable。

语言在比较 vtable 指针方面将走向何方仍是开放问题。 不难想象所有 vtable 都会获得某种生命周期擦除版的 TypeId,但结合下文讨论, 这可能并不像听起来那么直接。

向下转型方法不是 trait 方法

注意 Any trait 中唯一可用的方法是 type_id。 所有向下转型方法 直接在擦除的 dyn Any 上实现,或在 Box<dyn Any> 上,或在 dyn Any + Send 上等实现。向下转型对非擦除基类型通常没有意义——你已经知道它是什么!

另一个好理由是 Box<dyn Any> 等类型实现了 Any, 在 type_id 的情况下很容易意外调用 Box<dyn Any> 的实现而非 dyn Any 的实现。若 downcast_ref 也这样工作,情况会麻烦得多。

然而,这意味着将 Any 作为超 trait 并不能让你自己的 dyn Trait 支持向下转型。你必须先向上转型到 dyn Any,再向下转型,如我们之前所见。

但注意,超 trait Any 约束并非自定义向下转型的唯一方案!我们在下文探索另一种方法。

一些简短示例

此处我们看一些直接类型擦除和向下转型具体类型的简单示例。

基础

获取 dyn Any 与其他 trait 相同:

1
2
3
4
5
# use std::any::Any;
let mut i = 0;
let rf: &dyn Any = &();
let mt: &mut dyn Any = &mut i;
let bx: Box<dyn Any> = Box::new(String::new());

但要牢记 'static 要求:

1
2
3
4
5
# use std::any::Any;
let local = ();
let borrow = &local;
// 失败,因为 `borrow` 不是 `'static`
let _: &dyn Any = &borrow;

另一方面,dyn Any 总是 dyn Any + 'static, 这使得许多与 trait 对象相关的借用检查错误不可能发生。

尽管 Any 为 unsized 类型实现(即 unsized 类型也有 TypeId),类型擦除的 Sized 限制仍然适用:

1
2
3
# use std::any::Any;
// 失败,因为 `str` 不是 `Sized`
let _: &dyn Any = "";

可失败向下转型也相当直接。对引用,返回 Option:

1
2
3
4
5
6
7
8
9
# use std::any::Any;
# let mut i = 0;
# let rf: &dyn Any = &();
# let mt: &mut dyn Any = &mut i;
assert_eq!( rf.downcast_ref::<()>(), Some(&()) );
assert_eq!( rf.downcast_ref::<String>(), None );

assert!( mt.downcast_mut::<i32>().is_some() );
assert_eq!( mt.downcast_mut::<String>(), None );

对 Box,返回类型是 Result,以便在向下转型不适用时仍保留 Box<dyn Any> 的所有权。Ok 变体是 Box<T>,你可选择是否拆箱。

1
2
3
4
5
6
7
8
9
# use std::any::Any;
# let bx: Box<dyn Any> = Box::new(String::new());
if let Err(bx) = bx.downcast::<i32>() {
    println!("Hmm, not an `i32`.");
    if let Ok(bx) = bx.downcast::<String>() {
        let s: String = *bx;
        println!("Yep, it was a `String`.");
    }
}

基础部分到此为止!

TypeMap 模式

举一个更实用的例子,假设你想为可能遇到的每种不同类型存储一个 distinct 值,出于某种原因。也许你在存储同样类型擦除的回调,例如 Dog 的回调与 Cat 不同,甚至可能根本没有 Mouse 的回调。

一种做法是用将 TypeId 映射到值的数据结构。「类型映射」:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# use std::any::Any;
# use std::any::TypeId;
# use std::collections::HashMap;
pub struct TypeMap<V> {
    map: HashMap<TypeId, V>,
}

impl<V> TypeMap<V> {
    pub fn insert<T: Any>(&mut self, value: V) -> Option<V> {
        let id = TypeId::of::<T>();
        self.map.insert(id, value)
    }
    pub fn get_mut<T: Any>(&mut self) -> Option<&mut V> {
        self.get_mut_of(&TypeId::of::<T>())
    }
    pub fn get_mut_of(&mut self, id: &TypeId) -> Option<&mut V> {
        self.map.get_mut(id)
    }
    // ...
}

可用于回调思路:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# use std::any::Any;
# use std::any::TypeId;
# use std::collections::HashMap;
# pub struct TypeMap<V> {
#     map: HashMap<TypeId, V>,
# }
# impl<V> TypeMap<V> {
#     pub fn insert<T: Any>(&mut self, value: V) -> Option<V> {
#         let id = TypeId::of::<T>();
#         self.map.insert(id, value)
#     }
#     pub fn get_mut<T: Any>(&mut self) -> Option<&mut V> {
#         self.get_mut_of(&TypeId::of::<T>())
#     }
#     pub fn get_mut_of(&mut self, id: &TypeId) -> Option<&mut V> {
#         self.map.get_mut(id)
#     }
# }
pub struct Visitor {
    map: TypeMap<Box<dyn FnMut(&dyn Any)>>,
}

impl Visitor {
    // 因为我们返回先前创建的闭包,
    // 应注意不要*假定*回调中的参数是正确的类型。若从不让闭包逃逸到外部,
    // 可以安全假定参数确实是 `T`。
    //
    // 即使让闭包逃逸,若参数不是 `T` 则 `panic` 是健全的,
    // 但若让闭包逃逸,使用不稳定的 `downcast_unchecked` 则不健全。
    pub fn register<T, F>(&mut self, mut callback: F) -> Option<Box<dyn FnMut(&dyn Any)>>
    where
        T: Any,
        F: 'static + FnMut(&T),
    {
        let callback = Box::new(move |any: &dyn Any| {
            if let Some(t) = any.downcast_ref::<T>() {
                callback(t);
            }
        });

        self.map.insert::<T>(callback)
    }

    pub fn get_callback<T: Any>(&mut self) -> Option<impl FnMut(&T) + '_> {
        self.map
            .get_mut::<T>()
            .map(|f| {
                |t: &T| f(t)
            })
    }

    pub fn visit<T: Any>(&mut self, value: &T) -> bool {
        if let Some(mut callback) = self.get_callback::<T>() {
            callback(value);
            true
        } else {
            false
        }
    }

    pub fn visit_erased(&mut self, value: &dyn Any) -> bool {
        if let Some(callback) = self.map.get_mut_of(&value.type_id()) {
            callback(value);
            true
        } else {
            false
        }
    }
}

我们还类型擦除了回调签名,因为值需要单一类型。这在某种程度上是数据结构版的擦除 trait。

无论为何想按类型映射,这都是生态中已有的模式。

自定义向下转型

本示例探索如何为自己的 trait 添加(模拟的)向下转型,无需 Any 超 trait 约束,也无需对整个 trait 施加 'static 约束。实现仍依赖 TypeId,因此实际向下转型仍限于满足 'static 约束的类型。

方法是让 trait 中有方法返回实现类型的 TypeId,从而使基本思路可行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# use core::any::TypeId;
pub trait Trait {
    // 注意:我们稍后要重新审视此定义!
    fn type_id(&self) -> TypeId
    where
        Self: 'static
    {
        TypeId::of::<Self>()
    }
}

回想编译器对 Trait for dyn Trait 的实现会隐式向下转型接收者并调用此方法。因此我们现在可以从 dyn Trait 获取基类型的 TypeId。

然后可以实现自己的向下转型方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# use core::any::TypeId;
# pub trait Trait { fn type_id(&self) -> TypeId where Self: 'static { TypeId::of::<Self>() } }
// 注:这是 `dyn Trait + 'static` :-)
impl dyn Trait {
    pub fn is<T: 'static>(&self) -> bool {
        TypeId::of::<T>() == self.type_id()
    }

    pub fn downcast<T: 'static>(self: Box<Self>) -> Result<Box<T>, Box<Self>> {
        if (*self).is::<T>() {
            let ptr = Box::into_raw(self) as *mut T;
            // SAFETY: 继续阅读 :-)
            unsafe { Ok(Box::from_raw(ptr)) }
        } else {
            Err(self)
        }
    }
}

……对 downcast_ref、downcast_mut 以及 dyn Trait + Send、dyn Trait + Send + Sync 等更多实现同理。(是的,样板代码可能很多。)

然而,此示例有一个大的健全性漏洞。Trait 的实现者可以提供自己的 type_id 实现!他们可以重写默认体并返回非实现类型的 TypeId。这会使我们的 unsafe 产生 UB。由于 type_id 不是 unsafe 方法,责任在我们实现。

我们可以将方法标为 unsafe 并告诉 trait 使用者「别那样做」。但本示例中,我们将使重写默认体不可能。为此需要该方法为 final。嗯,Rust 还没有 final。然而,我们可以通过在方法中给出只能在我们模块内命名的参数来密封 该方法。

1
2
3
4
5
// 私有模块(无 `pub`)
mod private {
    // 包含 `pub` 类型(避免公共 trait 方法上的错误)
    pub struct Seal;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 pub trait Trait {
-    fn type_id(&self) -> TypeId
+    #[doc(hidden)]
+    fn type_id(&self, _: private::Seal) {
     where
         Self: 'static
     {
         TypeId::of::<Self>()
     }
 }

 impl dyn Trait {
     pub fn is<T: 'static>(&self) -> bool {
-        TypeId::of::<T>() == self.type_id()
+        TypeId::of::<T>() == self.type_id(private::Seal)
     }

现在若实现者试图写出 type_id 方法的签名,会得到隐私错误。

这是包含更多方法的 playground。 本示例基于 std::error::Error 及其自定义向下转型实现。

TypeId 的表示

TypeId 故意不透明且可能变更。它在相当长一段时间内内部用 u64 表示;然后在 Rust 1.72 表示改为 u128。 在 Rust 1.78 改为 u64 元组, 在 Rust 1.90 变为指针数组。 未来某个时点可能变成更奇特的东西。

简而言之,你不应依赖 TypeId 的确切表示,只应依赖其类型比较性质。

为什么是 'static?

Any trait 为所有满足 'static 约束的类型实现,不为其他类型实现;事实上它有 'static 约束,因而不能为不满足 'static 约束的类型实现。这意味着用 Any 模拟动态类型无法用于借用类型(除非借用的是 'static),例如。

为何如此严苛?简而言之,生命周期在运行前被擦除,具有不同生命周期的类型必须有相同的 TypeId 标识符,因此基于 TypeId 的向下转型会忽略生命周期并极度不健全。生命周期是类型的一部分,某些关系必须保留以保证健全性,但生命周期在运行前已擦除,无法动态保留这些关系。

因此没有健全的方式用 TypeId 或任何类似的生命周期无感知标识符直接执行非 'static 向下转型。

此 RFC PR 中有更多信息, 供好奇者阅读。注意该 PR 被接受但后来移除,且从未涉及非 'static 向下转型;它关于非 'static 的 type_id 方法。想法是获得忽略生命周期的「类型」标识符。

很大程度上因若存在此类东西,被以极度不健全的方式滥用的概率约为 100%。 而被撤回。

一种替代(如该线程所述)是某种动态检查两个类型(可能是泛型)是否相等,而不施加 'static 约束。该检查只对「固有 'static」的类型有意义,即根本不涉及任何生命周期参数的类型。在不实际暴露非 'static TypeId 或启用向下转型的情况下,这是可能的。

权衡导致相当违反直觉的行为:用此方法,&'static str 无法与 &'static str 比较,因为 &str 涉及生命周期参数!

另一种替代是提供某种本身为 'static 的「类型 lambda」,但能健全地将擦除的生命周期映射回正确位置。此处有草图, 但深入探讨超出本指南范围。

高阶类型相关考量

让我们花一分钟谈论在 Rust 中确实存在子类型与超类型关系的类型!高阶 类型具有这种关系。例如:

1
2
3
4
5
6
7
8
9
// 更明确地说,这是 `for<'any> fn(&'any str)` 函数指针。
// 该类型在生命周期上高阶。
let fp: fn(&str) = |_| {};

// 该类型是高阶类型的超类型。
let fp: fn(&'static str) = fp;

// 这会出错,因为不能健全地向下转型类型。
// let fp: fn(&str) = fp;

如前所述,这种子/超类型关系也适用于 dyn Trait 类型:

1
2
3
trait Look<'s> {}
type HR = dyn for<'any> Look<'any> + 'static;
type ST = dyn Look<'static> + 'static;

这里 HR 是 ST 的子类型,但不是同一类型。然而它们都满足 'static 约束:

1
2
3
4
5
6
7
# trait Look<'s> {}
# type HR = dyn for<'any> Look<'any> + 'static;
# type ST = dyn Look<'static> + 'static;
fn assert_static<T: ?Sized + 'static>() {}

assert_static::<HR>();
assert_static::<ST>();

作为 'static 类型,它们有 TypeId。作为不同类型,它们的 TypeId 不同,尽管一个是另一个的子类型。

这意味着你不能仅靠施加 'static 约束就停止考虑子类型与超类型。若要在泛型上下文中为健全性「禁用」子/超类型强制转换,必须使该上下文不变或采取其他步骤避免健全性漏洞,即使有 'static 约束。

见此 issue 了解此类健全性漏洞的真实案例,以及此评论 探讨高阶函数指针的子/超类型关系。

子类型与超类型仍是不同类型的事实,也意味着它们可能对除 Any 以外的 trait 有不同实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# trait Look<'s> {}
# type HR = dyn for<'any> Look<'any> + 'static;
# type ST = dyn Look<'static> + 'static;
#
# trait Trait {
#     fn say(&self);
# }
#
impl Trait for HR {
    fn say(&self) {
        println!("Meow");
    }
}

impl Trait for ST {
    fn say(&self) {
        println!("Woof");
    }
}

虽然这目前会产生未来兼容性警告, 计划是继续接受此模式。

const TypeId::of 的历史

自 Rust 1.91 起,TypeId::of 是 const fn。 (Any::type_id 仍不是 const,因为 trait 方法总体上尚不能为 const。)

稳定化历时良久,因为编译器不同部分对类型相等性的看法不同。当不同部分不一致时,在 const 上下文中结果可能不健全。

简要描述历史上的分歧,考虑以下示例。

1
2
3
4
5
6
let one: for<'a    > fn(&'a str, &'a str) = |_, _| {};
let two: for<'a, 'b> fn(&'a str, &'b str) = |_, _| {};
let mut fp = one;
fp = two;
let mut fp = two;
fp = one;

由于可变性,总是可以从其中一个调用另一个。它们可视为彼此的子类型。然而,与上文探讨的类似,它们是具有不同 TypeId 的不同类型。

在 PR 118247 之前,编译器部分会认为两种类型互为子类型,并将互为子类型的类型视为相等类型——我们称之为语义相等。然而,这是不健全的,因为 TypeId 基于语法相等。

该 PR 以语法解释解决了分歧。

上面的代码示例仍能编译,因此内部两种类型仍互为子类型,或即使不是子类型也存在其他强制转换。无论哪种情况,它们仍然是不同类型,可能令人困惑。

内置 dyn 实现优先级的考量

我们之前指出,即使你有应覆盖 dyn Trait 的 blanket 实现,Trait for dyn Trait 的内置实现通常仍优先。我们链接到此 issue 说明这在某些情况下目前不健全。

根据该 issue 周围的讨论,语言开发者希望在大多数情况下优先用户实现。然而这会破坏 Any trait!其实现 是与内置 dyn Any 实现重叠的 blanket 实现。但从 <dyn Any as Any>::type_id 返回 dyn Any 的 TypeId 会否定该 trait 的全部意义——获取擦除类型的 TypeId。

换言之,对 dyn Any(以及 Any 任何子 trait 的 dyn),内置实现继续优先至关重要。

事实上问题甚至更深。考虑我们(以及 std::error::Error!)的内置向下转型方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pub trait Trait {
    // SAFETY: 此方法必须返回基类型的 `TypeId` 以确保
    // 本模块 `is` 和向下转型方法的健全性。因此
    // 该方法被密封,使下面的默认体成为唯一可能的体。
    #[doc(hidden)]
    fn type_id(&self, _: private::Seal) -> TypeId
    where
        Self: 'static,
    {
        TypeId::of::<Self>()
    }
}

若有任何情况内置实现不优先,<dyn Trait>::type_id 将返回 dyn Trait 的 TypeId 而非擦除类型——即使此处没有超 trait 关系。

截至撰写时,有一种情况正在积极开发中——final 方法。 该功能的一种可能实现是 final 方法可从 trait 的 vtable 中排除。但实际上,这意味着默认方法体优先于内置 vtable 实现。

换言之,若 final 方法永不在 vtable 中, 将 Trait::type_id 设为 final 方法 作为密封方法的替代 无法工作。

这些问题在稳定化前如何影响 final 功能仍是开放问题。

最后修改 August 23, 2026: 更新 (499855b16)