我读过动态使编译器再次运行,但它做了什么。是否必须使用动态作为参数重新编译整个方法,或者更确切地说,那些具有动态行为/上下文的行(?)
事情是这样的。
对于每一个表达在动态类型的程序中,编译器会发出代码,生成表示操作的单个“动态调用站点对象”。因此,举例来说,如果您有:
class C
{
void M()
{
dynamic d1 = whatever;
dynamic d2 = d1.Foo();
那么编译器将生成道德上像这样的代码。 (实际代码要复杂得多;出于演示目的,对此进行了简化。)
class C
{
static DynamicCallSite FooCallSite;
void M()
{
object d1 = whatever;
object d2;
if (FooCallSite == null) FooCallSite = new DynamicCallSite();
d2 = FooCallSite.DoInvocation("Foo", d1);
看看到目前为止效果如何?我们生成调用站点once,无论您调用 M 多少次。调用站点在您生成一次后将永远存在。调用站点是一个对象,表示“这里将动态调用 Foo”。
好的,既然您已经获得了调用站点,那么调用是如何工作的呢?
调用站点是动态语言运行时的一部分。 DLR 说“嗯,有人试图在此对象上动态调用 foo 方法。我对此了解吗?不。那我最好找出来。”
然后,DLR 询问 d1 中的对象,看看它是否有什么特殊之处。也许它是旧版 COM 对象、Iron Python 对象、Iron Ruby 对象或 IE DOM 对象。如果它不是其中任何一个,那么它一定是一个普通的 C# 对象。
这是编译器再次启动的点。不需要词法分析器或解析器,因此 DLR 启动一个特殊版本的 C# 编译器,该编译器仅具有元数据分析器、表达式语义分析器以及发出表达式树而不是 IL 的发射器。
元数据分析器使用反射来确定 d1 中对象的类型,然后将其传递给语义分析器以询问在方法 Foo 上调用此类对象时会发生什么。重载解析分析器会计算出这一点,然后构建一个表达式树(就像您在表达式树 lambda 中调用 Foo 一样)来表示该调用。
然后,C# 编译器将该表达式树连同缓存策略一起传递回 DLR。该策略通常是“当你第二次看到这种类型的对象时,你可以重新使用这个表达式树,而不是再次给我回电”。然后,DLR 在表达式树上调用 Compile,这将调用表达式树到 IL 编译器并在委托中吐出动态生成的 IL 块。
然后,DLR 将此委托缓存在与调用站点对象关联的缓存中。
然后它调用委托,并发生 Foo 调用。
当您第二次致电 M 时,我们已经有了一个呼叫站点。 DLR 再次询问该对象,如果该对象与上次的类型相同,则会从缓存中获取委托并调用它。如果对象属于不同类型,则缓存未命中,整个过程重新开始;我们对调用进行语义分析并将结果存储在缓存中。
这发生在每一个表情这涉及到动态。例如,如果您有:
int x = d1.Foo() + d2;
那么有three动态调用站点。一种用于动态调用 Foo,一种用于动态加法,一种用于从dynamic 到 int 的动态转换。每一个都有自己的运行时分析和分析结果缓存。
合理?