Rust 动态分配具有通用参数的特征

Rust dynamic dispatch on traits with generic parameters

我有以下 Rust Playground permalink

来自我的追随者 Ray Tracing in a Weekend

在实施 material 时,我选择创建特征 Material

我的想法是,世界上的物体将具有 material 属性,并且在光线 HitHit 可以查看该物体并请求 material 一经请求。这对我遵循类似想法的 Normal 特征来说效果很好。我用动态调度实现了这一切,尽管我认为我掌握了足够的知识,也可以用特征边界静态地完成它。

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

如果我从 这样的问题中理解正确,那么似乎由于泛型是单态化的,Rust 运行时不可能泄露 material 的适当方法,因为表面上可能有一个不同的方法每种实现 Normal 的类型一个?这并不太符合我的要求,因为看起来,与 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,
}

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

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

put in the same position as the Rust runtime, I would be able to look at the Normal implementer I have in hand at a moment, say Sphere and say I will look in Sphere's vtable

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

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

但是,正如您已经指出的那样,由于泛型是单态的,因此 Normal 的每个实现都不会有一个 fn material:会有一个未知的,可能是无限的这些功能的家族,每个类型一个实现 Material。注意 "unknown, potentially infinite" 位:因为你不能泄漏隐私部分,如果 Normal 特征是 public,那么 Material 特征也必须是 public - 并且那么没有什么会阻止您的代码的用户添加另一个您的代码不知道的 Material 实现,它根本无法烘焙到 dyn Normal.

的 vtable 中

这就是泛型方法不是对象安全的原因。它们无法放入 trait 对象 vtable,因为在创建 trait 对象时我们并不知道它们。

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

我想你可以改变这个功能:

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

就目前而言,它表示:任何 Normal 提供一个函数 material,其中 caller 可以指定一个 Material 该函数将 return.

但是(我认为)你想声明的是:任何 Normal 都有一个 material 可以由调用者请求。但调用者无权指示任何将被 return 编辑的 Material。要将其转换为代码,您可以声明:

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

这表明 material return 是一个 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.