要记住多重分派的关键是它发生在 sub 或方法解析发生之后。所以所有的多次调度实际上是一个两步的过程。这两个步骤也是相互独立的。
当写类似的东西时:
multi sub foo($x) { }
multi sub foo($x, $y) { }
编译器会生成一个:
proto sub foo(|) {*}
也就是说,除非你自己写了一个proto 子。 proto 是实际安装到 lexpad 中的; multi sub 永远不会直接安装到 lexpad 中,而是安装到 proto 的候选列表中。
所以,当调用multi子时,过程是:
- 使用词法查找查找要调用的子程序,它解析为
proto
- 调用
proto,它会挑选出最好的multi 候选人并调用它
当嵌套作用域中有multi 候选对象时,来自外部作用域的proto 将被克隆并安装到内部作用域中,并将候选对象添加到克隆中。
一个非常相似的过程发生在多种方法上,除了:
- 多个方法只是存储在待办事项列表中,直到类、角色或语法的结束
}
-
proto 可能由角色或类提供,因此与 multi 候选者组成角色只需将它们也添加到待办事项列表中
- 最后,如果有多个方法没有
proto,但是一个父类有这样一个proto,就会被克隆;否则会生成一个空的proto
表示对多方法的调用是:
- 使用通常的方法分派算法(仅使用 C3 方法解析顺序搜索类)查找方法,该算法解析为
proto
- 调用
proto,它会选择最好的multi 候选人并调用它
完全相同的排序和选择算法用于多子和多方法。就多重分派算法而言,调用者只是第一个参数。此外,Perl 6 多分派算法不会比后面的参数更重视早期的参数,所以就像:
class A { }
class B is A { }
multi sub f(A, B) { }
multi sub f(B, A) { }
如果使用f(B, B) 调用,将被视为绑定,并给出一个模棱两可的调度错误,因此定义:
class B { ... }
class A {
multi method m(B) { }
}
class B is A {
multi method m(A) { }
}
然后调用 B.m(B),因为 multi-dipsatcher 再次看到类型元组 (A, B) 和 (B, A)。
Multiple dispatch 本身与窄的概念有关。如果 C1 的至少一个参数的类型比 C2 中相同位置的参数的类型更窄,并且所有其他参数都被绑定(即不窄,不宽),则候选 C1 比 C2 窄。如果反之为真,则它更宽。否则,它是绑定的。一些例子:
(Int) is narrower than (Any)
(Int) is tied with (Num)
(Int) is tied with (Int)
(Int, Int) is narrower than (Any, Any)
(Any, Int) is narrower than (Any, Any)
(Int, Any) is narrower than (Any, Any)
(Int, Int) is narrower than (Int, Any)
(Int, Int) is narrower than (Any, Int)
(Int, Any) is tied with (Any, Int)
(Int, Int) is tied with (Int, Int)
multi-dipsatcher 构建候选者的有向图,其中只要 C1 比 C2 窄,就会有一条从 C1 到 C2 的边。然后它找到所有没有传入边的候选,并将它们删除。这是第一批候选人。删除将产生一组没有传入边的新候选,然后将其删除并成为第二组候选。这种情况一直持续到所有候选者都从图中取出,或者如果我们达到无法从图中取出任何内容的状态(非常罕见的情况,但这将作为循环报告给程序员)。这个过程发生一次,而不是每次调度,它会产生一组候选者。 (是的,这只是一种拓扑排序,但分组细节对于接下来的内容很重要。)
当呼叫发生时,系统会按顺序搜索组以寻找匹配的候选人。如果同一组中的两个候选人匹配,并且没有决胜局(命名参数、where 子句或来自subset 类型、解包或is default 的隐含where 子句),则将报告模棱两可的调度.如果所有组都被搜索到没有结果,那么调度失败。
还有一些关于 arity(必需参数优于可选参数或 slurpy)和 is rw(它比没有 is rw 的其他同等候选者更窄)方面的一些狭隘考虑。
一旦发现一组中的一个或多个候选人匹配,就会考虑决胜局。其中包括命名参数、where 子句和解包的存在,并在首场比赛获胜的基础上工作。
multi f($i where $i < 3) { } # C1
multi f($i where $i > 1) { } # C2
f(2) # C1 and C2 tied; C1 wins by textual ordering due to where
请注意,此文本排序仅适用于平局;就类型而言,源代码中候选的顺序并不重要。 (命名参数也只能起到决胜局的作用,有时会让人感到意外。)
最后,我要指出,虽然多次分派的结果将始终与我所描述的两步过程相匹配,但实际上发生了大量的运行时优化。虽然所有查找最初都完全按照描述进行解析,但结果被放入调度缓存中,它提供的查找速度比搜索拓扑排序提供的组快得多。这是以这样一种方式安装的,即可以完全绕过 proto 的调用,从而节省调用帧。如果您--profile,您可以看到此行为的伪影;与多个候选者相比,任何基于类型的调度(没有决胜局)的自动生成的proto 将收到少量的调用。当然,如果您在 proto 中编写自定义逻辑,这将不适用。
除此之外,如果您在 MoarVM 上运行,动态优化器可以走得更远。它可以使用收集和推断的类型信息来解决方法/子调度和多调度,将2步过程变成0步过程。小的候选人也可以内联到调用者中(同样,分析器可以告诉您内联已经发生),这可以说将多调度变成了 -1 步过程。 :-)