4.3 dyn Trait 强制转换

译文 · 基于 Learning Rust

dyn Trait 强制转换

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

一些典型的(就强制转换内容而言)dyn Trait 强制转换如下:

1
2
3
4
5
6
# use std::sync::Arc;
# trait Trait {}
fn coerce_ref<'a, T: Trait + Sized + 'a>(t:    &T ) -> &(  dyn Trait + 'a) { t }
fn coerce_box<'a, T: Trait + Sized + 'a>(t: Box<T>) -> Box<dyn Trait + 'a> { t }
fn coerce_arc<'a, T: Trait + Sized + 'a>(t: Arc<T>) -> Arc<dyn Trait + 'a> { t }
// 等等

实践中通常比这些更简洁,因为我包含了一些通常隐含或不使用的显式生命周期和约束。例如泛型类型参数上的 Sized 约束通常隐含,但我显式写出以强调我们讨论的是 Sized 基础类型。

要点是:给定 dyn 兼容的 Trait,且当 T: 'a + Trait + Sized 时,可将 Ptr<T> 强制转换为 Ptr<dyn Trait + 'a>,其中 Ptr 为 &_、Box<_> 等支持的指针类型。

若需要 dyn Trait + Send + 'a,自然还需要 T: Send,其他自动 trait 同理。

本节其余部分探讨这些典型例子之外的情况,以及强制转换的一些限制。

关联类型

当 trait 有一个或多个非泛型关联类型时,每个具体实现者为每个关联类型选择单一、静态已知的类型。对基础类型,关联类型是实现类型和 trait 的「输出」:知道后两者即可静态确定关联类型。

那么 Trait 对 dyn Trait 的实现中关联类型应该是什么?

没有单一答案;它们需要随擦除的基础类型而变化。

然而,带关联类型的 trait 的 dyn Trait 太有用了,不能因此使带关联类型的 trait 无法用于 dyn Trait。相反,trait 中的关联类型实质上成为 dyn Trait 类型构造器的命名类型参数。(回想它已因 trait 对象生命周期而是类型构造器。)

给定

1
2
3
4
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

我们有

1
2
3
dyn Iterator<Item = String> + '_
dyn Iterator<Item = i32> + '_
dyn Iterator<Item = f64> + '_

等等。dyn Trait<...> 中的关联类型必须解析为具体类型,dyn Trait<...> 才是具体类型。

自然,只有既实现 Iterator 又在实现中 type Item = String 时,才能强制转换为 dyn Iterator<Item = String>。语法与关联类型 trait 约束相同:

1
2
3
4
5
6
fn takes_string_iter<Iter>(i: Iter)
where
    Iter: Iterator<Item = String>,
{
   // ...
}

参数被命名有多方面好处。通常很相关,例如 Iterator 返回什么 Item(尤其关联类型命名良好时)。也无需以良好定义的方式排序关联类型,如字典序或声明顺序(后者太脆弱)。

命名参数必须在所有有序参数之后指定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
trait AssocAndParams<T, U> { type Assoc1; type Assoc2; }

// trait 的有序类型参数必须按声明顺序
//(此处 `String` 然后 `usize`)。之后是命名的关联类型参数,
// 彼此之间可任意重排。
fn foo(d: Box<dyn AssocAndParams<String, usize, Assoc1 = i32, Assoc2 = u32>>)
->
    Box<dyn AssocAndParams<String, usize, Assoc2 = u32, Assoc1 = i32>>
{
   d
}

选择退出 dyn 可用性

自 Rust 1.72 起, 若对关联类型添加 where Self: Sized 约束,则视为非 dyn 可用。关联类型对 dyn Trait 不可用,也不再需要用命名参数约束。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
trait Trait {
    type Foo where Self: Sized;
    fn foo(&self) -> Self::Foo where Self: Sized;
    fn bar(&self) {}
}

impl Trait for i32 {
    type Foo = ();
    fn foo(&self) -> Self::Foo {}
}

impl Trait for u64 {
    type Foo = f32;
    fn foo(&self) -> Self::Foo { 0.0 }
}

// 不需要 `dyn Trait<Foo = ()>`!
let mut a: &dyn Trait = &0_i32;

// 基础类型之间不需要关联类型相等!
a = &0_u64;

// 失败,因为类型未定义(`dyn Trait` 不是 `Sized`)
// let _: <dyn Trait as Trait>::Foo = todo!();

虽然会产生警告,仍可可选指定关联类型,尽管 dyn Trait 本身不可用。注意这会导致不兼容类型并限制可能的强制转换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# trait Trait {
#    type Foo where Self: Sized;
#    fn foo(&self) -> Self::Foo where Self: Sized;
#    fn bar(&self) {}
# }
# impl Trait for i32 {
#    type Foo = ();
#    fn foo(&self) -> Self::Foo {}
# }
# impl Trait for u64 {
#    type Foo = f32;
#    fn foo(&self) -> Self::Foo { 0.0 }
# }
let mut a: &dyn Trait<Foo = ()> = &0_i32;

// 失败!
a = &0_u64;

这为为 Box<dyn Trait> 实现 trait 带来一些有趣可能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# trait Trait {
#    type Foo where Self: Sized;
#    fn foo(&self) -> Self::Foo where Self: Sized;
#    fn bar(&self) {}
# }
# impl Trait for i32 {
#    type Foo = ();
#    fn foo(&self) -> Self::Foo {}
# }
# impl Trait for u64 {
#    type Foo = f32;
#    fn foo(&self) -> Self::Foo { 0.0 }
# }
impl<T: Default> Trait for Box<dyn Trait<Foo = T>> {
    type Foo = T;
    fn foo(&self) -> Self::Foo {
        T::default()
    }
}

警告目前说「可以指定关联类型但不能使用」,但此例表明技术上不成立。我认为这类用法未被预料。

以这种方式指定非 dyn 可用关联类型不是错误,是因为曾有一段时间可以为关联类型添加 Self: Sized 约束,但仍必须在 dyn Trait<..> 中命名关联类型。因此将警告改为错误会是破坏性变更。

鉴于潜在用处,我认为警告至少应改写,或许应重命名。

无嵌套强制转换

unsizing 强制转换需要在间接层之后发生(如引用或 Box 中),以容纳擦除类型 vtable 的宽指针(且不支持移动非大小类型)。

然而,unsizing 强制转换通常只能在单层间接之后发生。例如不能将 Vec<Box<T>> 强制转换为 Vec<Box<dyn Trait>>。为什么?Box<T> 和 Box<dyn Trait> 布局不同!前者是一个指针大小,后者是两个指针。整个 Vec 需要重新分配以适应这种变化:

1
2
3
4
# trait Trait {}
fn convert_vec<'a, T: Trait + 'a>(v: Vec<Box<T>>) -> Vec<Box<dyn Trait + 'a>> {
    v.into_iter().map(|bx| bx as _).collect()
}

一般而言,unsizing 强制转换消耗原指针(引用、Box 等)并产生新指针,这在嵌套上下文中无法发生。

内部哪些强制转换可能由 CoerceUnsized trait 和(编译器实现的)Unsize trait 决定,见文档。

例外情况

有些实质性和表面性例外,unsizing 强制转换可在嵌套上下文中发生。

若跟随上面链接,会看到某些类型如 Cell 以递归方式实现 CoerceUnsized。想法是 Cell 等与其泛型类型参数布局相同。因此外层 Cell 不算「嵌套」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# use std::cell::Cell;
# trait Trait {}
// 失败 :-(
//fn coerce_vec<'a, T: Trait + 'a>(v: Vec<Box<T>>) -> Vec<Box<dyn Trait + 'a>> {
//    v
//}

// 成功!:-)
fn coerce_cell<'a, T: Trait + 'a>(c: Cell<Box<T>>) -> Cell<Box<dyn Trait + 'a>> {
    c
}

我们将在 upcoming 章节涵盖其他表面例外(实际是超类型强制转换)。

Sized 限制

基础类型必须满足 Sized 约束才能强制转换为 dyn Trait。例如 &str 不能强制转换为 &dyn Display,尽管 str 实现 Display,因为 str 是非大小的。

为何有此限制?&str 也是宽指针;由指向 UTF-8 字节的指针和字节数的 usize 组成。类似地切片引用 &[T] 是指向连续数据的指针和元素个数。

从 &str 或 &[T] 创建的 &dyn Trait 天真地需要「超宽指针」:数据指针、元素计数和 vtable 指针。但 &dyn Trait 是静态布局的具体类型——两个指针——因此天真方法行不通。此外,若要强制转换超宽指针呢?每次递归强制转换都需要另一个指针,大小无界。

非天真方法需要为擦除的非 Sized 基础类型的动态分发特殊处理。例如类型擦除 str 后,已丢失 &str 也是宽指针及如何创建该宽指针的信息,但代码需要重建宽指针才能动态分发。

因此若要非天真支持非大小类型,dyn Trait 需要在运行时检查如何构造指向擦除基础类型的指针:瘦指针一种可能,每种支持的宽指针类型各一种可能。不仅如此,所需元数据(如 str 长度)必须存储在某处,不能像 vtable 那样在静态内存中。

因此 simply 不支持非大小基础类型。

有时可通过变通,例如为 &str 而非 str 实现 trait,然后将 &'_ str 强制转换为 dyn Trait + '_(因为引用总是 Sized)。

1
2
3
4
5
6
7
8
# use std::fmt::Display;
// 失败:无法将 `str` 强制转换为 `dyn Display`,因此无法将
// `&str` 强制转换为 `&dyn Display`。
// let _: &dyn Display = "hi";

// 但 `&str` 也实现 `Display`。(若 `T: Display`,则 `&T: Display`。)
// 因为 `&str` 是 `Sized`,可以将 `&&str` 强制转换为 `&dyn Display`:
let _: &dyn Display = &"hi";

Sized 也用作某种「非 dyn」标记,我们稍后探讨。

Sized 限制有一个广泛例外:在 dyn Trait 形式之间强制转换,见下面小节。

丢弃自动 trait

可以将 dyn Trait + Send 强制转换为 dyn Trait,类似地可丢弃任何其他自动 trait。

尽管dyn Trait 不是 dyn Trait + Send 的超类型, 这仍称为将 dyn Trait + Send 向上转型为 dyn Trait。

自动 trait 没有方法,因此这些强制转换不需要改变 vtable。它们允许从更受限的函数(如需要 dyn Trait + Send)调用限制更少的函数(接受 dyn Trait)。强制转换是必要的,因为再次强调,这些是(不同的)具体类型,不是泛型、子类型或动态类型。

虽然不需要改变 vtable,此强制转换仍不能在嵌套上下文中发生。

丢弃主 trait

自 Rust 1.84 起, 也可丢弃主 trait 创建类似 dyn Send 的类型,若出于某种原因需要。可通过改变指针实现,也可保持指针不变;此类强制转换后唯一能动态分派的方法是调用析构函数。

自反情况

可以将 dyn Trait 强制转换为 dyn Trait。

抱歉,我们又不够精确。可以将 dyn Trait + 'a 强制转换为 dyn Trait + 'b,其中 'a: 'b。这对dyn Trait + '_ 的借用如何工作很重要。

生命周期在编译时擦除,vtable 与生命周期无关。尽管如此,此 unsizing 强制转换仍不能在嵌套上下文中发生。

然而,在后续关于型变的章节 我们将看到,在协变上下文中,方差甚至允许在嵌套上下文中缩短 trait 对象生命周期。之后关于高阶类型的章节 探讨另一种可视为自反的生命周期相关强制转换。

超 trait 向上转型

自 Rust 1.86 起 也可将(「向上转型」)dyn SubTrait 强制转换为 dyn SuperTrait。

再次明确,这是强制转换而非实际的子/超类型关系,尽管术语如此。虽是实现细节,转换可能涉及替换 vtable 指针(与丢弃自动 trait 或自反情况对比)。

你也可以编写自己的「手动」超 trait 向上转型(在 trait 向上转型稳定化之前更重要)。

仅 dyn 兼容的 trait

trait 上还有其他限制未在此讨论,例如(尚)不支持带 GAT 的 trait。我们在下一节涵盖。

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