4.4 dyn 兼容性(对象安全)

译文 · 基于 Learning Rust

dyn 兼容性(对象安全)

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

存在无法创建 dyn Trait 的 trait:

1
2
let s = String::new();
let d: &dyn Clone = &s;

此处不重复所有规则,直接链接参考手册。 请先阅读。(如参考手册所述,dyn 兼容性曾称为对象安全。本指南早期版本使用「dyn 安全」一词。)

截至本文写作时,参考手册尚未更新:可通过添加 where Self: Sized 约束使关联类型和GAT对 trait 对象不可用。暂称此为将 GAT(或关联类型)选择退出「dyn 可用」。

可能不明显的是为何存在这些限制。本页其余部分探讨部分原因。

Sized 约束

在讨论限制之前,先说明 Sized 约束如何与 dyn Trait 和 dyn 兼容性配合。

Rust 用 Sized 表示:

  • trait 不是 dyn 兼容的
  • 关联类型或GAT不是 dyn 可用的
  • 方法不是 dyn 可分发的
  • 关联函数对 dyn Trait 不可调用
    • 尽管目前永远不行,为明确和未来兼容仍需声明

这有一定道理,因为 dyn Trait 不是 Sized。因此 dyn Trait 不能实现以 Sized 为超 trait 的 trait,dyn Trait 也不能调用需要 Sized 的方法或关联函数。

然而,这仍是权宜之计,因为存在不是 Sized 但也非 dyn Trait 的类型,我们可能想为它们实现 trait,包括不可 dyn 分发的方法(如泛型方法)。目前在 Rust 中不可能(不可 dyn 分发的方法对其他实现该 trait 的非大小类型也不可用)。

若想阅读关于此权宜之计局限性的吐槽,请展开。

考虑此例,为保持 dyn 兼容而添加 Sized 约束:

1
2
3
4
5
# trait Bound<T: ?Sized> {}
trait Trait {
    // 因方法是泛型的而选择退出 `dyn` 可分发性
    fn method<T: Bound<Self>>(&self) where Self: Sized;
}

若尝试为 str 实现此 trait,即使有 method 在逻辑上合理,也无法使用。可以这样写实现,但不能调用,因为 trait 上的约束优先。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# trait Bound<T: ?Sized> {}
# trait Trait {
#    fn method<T: Bound<Self>>(&self) where Self: Sized;
# }
// 能编译...
impl Trait for str {
    // 注:实现中省略了约束。
    fn method<T: Bound<Self>>(&self) {
        println!("Hi there");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# trait Bound<T: ?Sized> {}
# trait Trait {
#    fn method<T: Bound<Self>>(&self) where Self: Sized;
# }
# impl Trait for str {
#     fn method<T: Bound<Self>>(&self) {
#         println!("Hi there");
#     }
# }
# impl Bound<str> for i32 {}
// ...但方法不可调用。
"".method::<i32>()

不如直接省略。

1
2
3
4
5
6
7
# trait Bound<T: ?Sized> {}
# trait Trait {
#    fn method<T: Bound<Self>>(&self) where Self: Sized;
# }
impl Trait for str {
    // Rust 1.87 起可以省略方法
}

情况相当糟糕。理想情况下应有独立 trait 选择退出 dyn 兼容性和可分发性,而非用 Sized;可称 NotDyn。然后可有 Sized: NotDyn 保持兼容,将上面约束改为 NotDyn,str 的实现就能工作。

情况已有改善,未来可能更多:

但我认为消除 dyn 兼容性与 Sized 的混淆会更清晰正确,无论未来有何变通。

接收者限制

dyn 可分派方法需要某种基于 Self 的接收者,是为了确保 vtable 可用。需要存在指向 Self 的某种宽指针,才能找到 vtable 并执行动态分发。

可以说可扩展到接受单个非接收者 &Self 等方法。

至于接收者类型的其他限制,编译器必须知道如何从类型擦除版本回到原始版本才能实现 Trait。未来可能泛化,但目前是受限集合。

泛型方法限制

要支持类型泛型方法,vtable 中需要泛型可能采用的每种类型的函数指针。这不仅使 vtable 庞大,还需要某种全局分析。毕竟使用你 trait 的每个 crate 都可能定义满足约束的新类型,他们(或你)也可能想用那些类型调用该方法。

有时可通过擦除问题中的泛型类型参数来绕过(在主方法中、作为替代方法,或在不同的「擦除」trait 中)。我们稍后有一个示例

使用 Self 的限制

以接收者以外形式接受 Self 的方法可能依赖参数与实现类型完全相同。基础类型擦除后无法依赖这一点。

例如 PartialEq<Self>:

1
2
3
4
// 简化
pub trait PartialEq {
    fn partial_eq(&self, rhs: &Self);
}

若为 dyn PartialEq 实现,rhs 参数会像 self 一样是 &dyn PartialEq。但没有保证基础类型相同!u8 和 String 都实现 PartialEq,但没有设施比较它们是否相等(Rust 也不打算任意处理)。

有时可为其他 dyn Trait 提供自己的实现,可能利用 Any trait 模拟动态类型和反射。我们稍后给出此方法的示例

impl Clone for Box<dyn Trait> 示例 演示了 Self 作为返回值的情况。

GAT 限制

GAT 太新,尚不支持类型擦除。需要某种方式将 GAT 嵌入 dyn Trait 作为参数,类似非泛型关联类型的做法。

自 Rust 1.72 起, 可通过添加 Self: Sized 约束选择退出 GAT 的 dyn 可用性,从而不必将 GAT 命名为参数。

这与非泛型关联类型的相同能力类似。有趣的是,不仅可以指定具体 GAT 相等……

1
2
3
4
5
6
7
8
9
trait Trait {
    type Gat<'a> where Self: Sized;
}

impl Trait for () {
    type Gat<'a> = &'a str;
}

let _: &dyn Trait<Gat<'static> = &'static str> = &();

……还有高阶 GAT 相等:

1
2
3
4
5
6
7
8
# trait Trait {
#    type Gat<'a> where Self: Sized;
# }
# impl Trait for () {
#    type Gat<'a> = &'a str;
# }
// 此语法仍不支持
let _: &dyn Trait<for<'a> Gat<'a> = &'a str> = &();
1
2
3
4
5
6
7
8
# trait Trait {
#    type Gat<'a> where Self: Sized;
# }
# impl Trait for () {
#    type Gat<'a> = &'a str;
# }
// 但对 `dyn Trait`,可将绑定移到 `Trait` 外:
let _: &dyn for<'a> Trait<Gat<'a> = &'a str> = &();

但与非泛型关联类型情况一样,任何对等式的使用都必须间接完成,因为 dyn Trait 本身不能在自己的实现中定义 GAT。

关联常量限制

类似地,支持关联常量至少需要关联常量相等支持。

返回位置 impl Trait 限制

使用RPIT 的 trait 方法,概念上是声明不透明关联类型或 GAT 的语法糖。此外,即使 RPIT 不捕获泛型参数因而对应返回关联类型,目前也无法命名该关联类型。

类似泛型方法,有时可通过擦除返回类型绕过。(注意有权衡,但讨论更适合专门的 RPIT 指南。)

历史

对象安全在 RFC 0255 中引入, RFC 0546 移除了 trait 上的隐式 Sized 约束,并添加有(显式)Sized 约束的 trait 不是对象安全的规则。

两个 RFC 都在 Rust 1.0 之前实现。

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