【问题标题】:Rust dynamic dispatch on traits with generic parameters使用泛型参数对特征进行 Rust 动态调度
【发布时间】:2020-04-25 02:55:07
【问题描述】:

我有以下 Rust Playground permalink

来自我的关注Ray Tracing in a Weekend

在实现材料时,我选择创建一个特征Material

我的想法是,世界上的物体将具有material 属性,并且在射线上Hit Hit 可以查看物体并按需请求材料。这适用于我的 Normal 特质,它遵循类似的想法。我用动态调度实现了这一切,尽管我认为我已经掌握了足够的能力,也可以用 trait bounds 静态地完成它。

在链接代码中,您会看到第 13 行,我要求那些实现 Normal 的人有一个返回 Material 的方法。这表明现在Normal 不再有资格成为特征对象error[E0038]: the traitNormalcannot be made into an object

如果我从this 这样的问题中正确理解,似乎由于泛型是单态的,Rust 运行时不可能泄露material 的适当方法,因为表面上可能对于实现@ 的每种类型都有一个不同的方法987654335@?这似乎不太符合我的要求,因为看起来,放在与 Rust 运行时相同的位置,我将能够查看我手头的 Normal 实现者,比如 Sphere 并说我将查看Sphere 的vtable。你能解释一下我在哪里错了吗?

从那里,我尝试简单地与编译器抗争并进行静态调度。 第 17-21 行

struct Hit<'a> {
    point: Vec3,
    distance: f32,
    object: &'a dyn Normal,
}

成为

struct Hit<'a, T: Normal> {
    point: Vec3,
    distance: f32,
    object: &'a T,
}

从那里开始,我试图一个接一个地堵住一个又一个洞,似乎看不到尽头。

除了了解我目前的理解存在根本性错误之外,我还可以做哪些不同的设计选择?

【问题讨论】:

    标签: generics rust dispatch


    【解决方案1】:

    我可能遗漏了一些东西,但我认为你可以 - 至少从我所看到的 - 沿着你的道路走得更远。

    我认为您可以更改此功能:

    fn material<T: Material>(&self) -> T;
    

    就目前而言,它说:任何Normal 都提供了一个函数material调用者可以指定该函数将返回的Material

    但是(我认为)您要声明的是:任何Normal 都有一个可以由调用者请求的材料。但是调用者无权指定将返回的任何Material。要将其转换为代码,您可以声明:

    fn material(&self) -> &dyn Material;
    

    这表明material 返回一个Material(作为特征对象)。

    那么,Sphere 可以实现Normal

    impl<'a> Normal for Sphere<'a> {
        fn normal(&self, point: &Vec3) -> Ray {
            Ray::new(point, &(point - &self.center))
        }
        fn material(&self) -> &dyn Material {
            self.material
        }
    }
    

    Link to playground.

    【讨论】:

    • 在这种情况下,使用关联类型可能会更好(如果某些实现需要,可能是dyn Material)。
    • 这项技术在我的尝试中发挥了作用,并与上面的@Cerberus 回答相结合,我理解为什么,但是与他在这里提到的使用关联类型相比,这样做有什么优势吗?
    • 这是一种权衡:这种技术允许您发明新的Materials,而无需重新编译Sphere 等。 - 泛型往往会渗透到代码库中(这本身并不坏——正如所说,这是一种权衡)。
    【解决方案2】:

    放在与 Rust 运行时相同的位置,我可以立即查看我手头的 Normal 实现器,说 Sphere 并说我将查看 Sphere 的 vtable

    除了这里 Rust 运行时无能为力。

    事实上,Rust 没有“执行代码的东西”意义上的运行时。 Rust 运行时只执行设置和清理任务,但只要控制流在您的 main 函数中的某个位置,您就只能靠自己了(在 no_std 环境中,即使这样也不存在)。因此,每个动态调度都必须通过将 vtable 指针放置在数据指针旁边来烘焙到类型中 - 有关更多详细信息,请参阅 this documentation

    但是,正如您已经说过的那样,由于泛型是单态的,因此对于 Normal 的每个实现都不会有一个 fn material:将有一个未知的,可能是无限的这些函数系列,一个对于实现Material 的每种类型。请注意“未知,可能无限”位:由于您不能泄漏私有部分,因此如果 Normal 特征是公开的,则 Material 特征也必须是公开的 - 然后没有什么会阻止您的代码的用户添加Material 的另一种实现,您的代码不知道,它根本无法被烘焙到 dyn Normal 的 vtable 中。

    这就是为什么泛型方法不是对象安全的。它们不适合 trait 对象 vtable,因为在创建 trait 对象时我们并不知道它们。

    【讨论】:

    • 感谢您的明确解释。我现在肯定看到在 Normal 公开的情况下,由于无法明确确定 vtable 是如何烘焙的,但是没有实现的数量在编译时固定的情况等等查找将被修复?编译器是否仍然保留它以防止您提到的情况?
    • AFAIK,理论上这可以是特例,在这个方向上根本没有任何工作。
    • @CircArgs 如果实现的数量是固定的,您通常应该使用enum,这在这里可以正常工作。
    • @trentcl 实现的数量实际上是为了固定至少我在这里玩弄的。换句话说,我打算只拥有固定数量的Materials。你是说有一个材料的枚举并且它们暗示了枚举?如果是这样,不同的材料会有不同的行为吗?
    • @CircArgs 是的,类似于this。您可以进一步分解matches 并将手臂分解为单独的功能,如果有一个大的太多了。我的评论的重点是,如果您正在编写编译器可以“烘焙”vtable 的代码,那么您基本上是在编写带有额外步骤的enum
    猜你喜欢
    • 1970-01-01
    • 2022-10-05
    • 1970-01-01
    • 2023-01-12
    • 2022-11-23
    • 2020-12-24
    • 2022-12-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多