【问题标题】:Compiler generated sealed class for delegate keyword contains virtual methods编译器为委托关键字生成的密封类包含虚方法
【发布时间】:2012-07-09 22:26:49
【问题描述】:

在 C# 中使用 delegate 关键字时,C# 编译器会自动生成一个派生自 System.MulticastDelegate 类的类。

这个编译器生成的类也包含 3 个方法:Invoke, BeginInvoke and EndInvoke

所有这三个方法都标记为public virtual extern,但有趣的是类本身标记为sealed

在密封类中定义的虚拟方法不仅违反直觉,而且在 C# 中实际上是非法的。

所以我的问题是,这是否有特定的原因,或者它只是在记住一些假设的未来增强的情况下所做的那些无害的事情之一?

编辑 1:

原因可能是强制使用“callVirt”IL 操作码而不是“call”,以便在尝试执行这三种方法中的任何一种之前,CLR 始终检查委托对象是否为空?虽然我不明白为什么 delegate 在这方面应该是一个特例。

强制使用callvirt 也不会影响性能(尽管可能微不足道)

编辑 2:

添加了 CIL 标记,事实证明 C# 定义委托的方式实际上是由 CIL 标准强制执行的。标准规定(以下不是全文)

委托应具有 System.Delegate 的基本类型。代表应 被宣布密封,并且代表唯一的成员是 此处指定的前两种或所有四种方法。这些 方法应声明为运行时和管理。他们不得有 主体,因为该主体应由 VES 自动创建。其他 委托上可用的方法是从类继承的 基类库中的 System.Delegate。委托方法是:

  1. 实例构造函数
  2. Invoke 方法应该是虚拟的
  3. BeginInvoke 方法(如果存在)应为虚拟
  4. EndInvoke 方法应为虚拟

所以这绝对不是编译器过程的副作用,或者类似于其他有趣的编译器输出。

如果标准强调某事,那一定是有充分的理由和理由。

那么现在的问题是,为什么 CIL 代表标准同时强调密封和虚拟?

问题在这里吗?:

他们不应该有身体,因为身体应该由 VES 自动创建。

它们是否被标记为虚拟,以便在调用这些方法时可以执行 VES/CLR 生成的主体?

【问题讨论】:

  • 不要忘记,在 C# 中无效的内容在 IL 中可能完全有效 - 目前还有其他实例,这可能是另一个。但是对于有趣的发现 +1。
  • 我用 ILDasm 进行了尝试,并且在 IL 中将 override 关键字转换为 virtual。
  • @MBen 是的,但是对于 C# 生成的委托类,没有相应的基类方法可以覆盖。因此这里的虚拟关键字不是“覆盖”的结果:)

标签: c# .net delegates cil il


【解决方案1】:

正如我在问题中指出的那样,这种密封的虚拟异常实际上是由 CIL 标准规定的。目前尚不清楚为什么 CIL 标准特别提到委托方法 InvokeBeginInvokeEndInvoke 应该是虚拟的,同时又强制密封 Delegate 继承的类。

此外,通过 SSCLI 代码后,我了解到 JIT 编译器的内部优化会自动将密封类的虚拟方法上的任何 callvirt 调用转换为带有额外空值检查的正常调用。这意味着,尽管在 IL 中被标记为虚拟,但当通过 callvirt 指令调用其 Invoke(或其他两个)方法时,委托不会受到任何性能影响。

当一个委托的调用被调用时,CLR 会自动为这个方法发出一个高度优化的主体,而不是编译 IL 代码来生成它为“普通”方法所做的主体。这与在 IL 中标记为 virtual 无关。

我还通过手动修改 IL 代码并重新组装它来验证 virtual 可以安全地从生成的委托类的 IL 代码中删除。尽管违反了 CIL 标准,但生成的程序集运行良好。

.class private auto ansi beforefieldinit MainApp
       extends [mscorlib]System.Object
{
  .class auto ansi sealed nested private Echo
         extends [mscorlib]System.MulticastDelegate
  {
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(object 'object',
                                 native int 'method') runtime managed
    {
    } // end of method Echo::.ctor

    .method public hidebysig instance int32  Invoke(int32 i) runtime managed
    {
    } // end of method Echo::Invoke

    .method public hidebysig instance class [mscorlib]System.IAsyncResult 
            BeginInvoke(int32 i,
                        class [mscorlib]System.AsyncCallback callback,
                        object 'object') runtime managed
    {
    } // end of method Echo::BeginInvoke

    .method public hidebysig instance int32  EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed
    {
    } // end of method Echo::EndInvoke

  } // end of class Echo

  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       34 (0x22)
    .maxstack  3
    .locals init ([0] class MainApp app,
             [1] class MainApp/Echo dele)
    IL_0000:  nop
    IL_0001:  newobj     instance void MainApp::.ctor()
    IL_0006:  stloc.0
    IL_0007:  ldloc.0
    IL_0008:  ldftn      instance int32 MainApp::DoEcho(int32)
    IL_000e:  newobj     instance void MainApp/Echo::.ctor(object,
                                                           native int)
    IL_0013:  stloc.1
    IL_0014:  ldloc.1
    IL_0015:  ldc.i4.5
    //callvirt can also be replaced by call without affecting functionality
    // since delegate object is essentially not null here
    IL_0016:  callvirt   instance int32 MainApp/Echo::Invoke(int32)
    IL_001b:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0020:  nop
    IL_0021:  ret
  } // end of method MainApp::Main

  .method private hidebysig instance int32 
          DoEcho(int32 i) cil managed
  {
    // Code size       7 (0x7)
    .maxstack  1
    .locals init ([0] int32 CS$1$0000)
    IL_0000:  nop
    IL_0001:  ldarg.1
    IL_0002:  stloc.0
    IL_0003:  br.s       IL_0005

    IL_0005:  ldloc.0
    IL_0006:  ret
  } // end of method MainApp::DoEcho

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method MainApp::.ctor

} // end of class MainApp

请注意,我已将虚拟方法转换为普通实例方法。

由于这个更改后的 IL 运行得非常好,它证明了密封委托类中的标准强制虚拟方法不是必需的。它们也可以是普通的实例方法。

所以这个异常很可能是强调调用这三个委托方法实际上会导致调用其他一些方法(即运行时多态性,就像“正常”虚拟方法一样),或者这是为了适应未来与代表相关的一些假设增强。

【讨论】:

  • 你能参考一下你在哪里了解到“反对编译 IL 的高度优化的主体”吗?
【解决方案2】:

你被你用来查看类型定义的反汇编程序绊倒了。必须将 IL 翻译回可识别的语言,例如 C#。这通常不可能完全保真,IL 的规则与 C# 语言规则相同。这不仅仅发生在委托身上,接口实现方法也是虚拟的,即使您没有在 C# 代码中声明它是虚拟的。

为了进一步搅浑水,如果编译器可以从代码分析中确定目标对象,IL 实际上允许编译器发出对虚拟方法的非虚拟调用。但是对于委托或接口调用,这永远不会发生。而且 IL 允许对非虚拟方法进行虚拟调用,这是 C# 编译器使用 gusto 实现的保证,即永远不能使用 null this 调用实例方法。

但是 C# 的使用是一个聪明的技巧,只有在设计 CLR 之后才发现。 virtual 的初衷当然是要注释该方法应该使用 Callvirt 调用。最终这并不重要,因为编译器知道委托和接口行为,并且总是会发出 Callvirt。并且实际的方法调用是在假定 Callvirt 激活的 CLR 代码中实现的。

【讨论】:

  • 感谢您的解释。请参阅我对问题的编辑2。
【解决方案3】:

它似乎不是特定于代表的。 我试过这个例子:

 public abstract class Base
    {
        public abstract void Test();
    }

    public sealed class Derived : Base
    {
        public override  void Test()
        {
            throw new NotImplementedException();
        }
    }

在 ILDasm 中,我得到这个是为了实现 Test() :

.method public hidebysig virtual instance void 
        Test() cil managed
{
  // Code size       7 (0x7)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  newobj     instance void [mscorlib]System.NotImplementedException::.ctor()
  IL_0006:  throw
} // end of method Derived::Test

可能是 override 关键字不是 CLR 关键字。

【讨论】:

  • 感谢您的回复。这是另一个非常有趣的发现,但与委托的区别在于我提到的三个方法是“新”生成的方法而不是覆盖。覆盖本质上是虚拟的,正如你提到的,我也认为 MSIL 没有相应的覆盖关键字。
  • 只是我刚刚学到的一点。 virtual 之前没有关键字“newslot”表示该 virtual 方法是一个覆盖...
【解决方案4】:

这是编译过程的副作用。我不知道确切的原因,这种行为还有更多的例子。例如,一个编译后的静态类变成了一个抽象的密封类(所以你不能创建它的实例,也不能从它继承)。

【讨论】:

  • 关于静态类的非常有趣的一点:)...但我认为抽象密封几乎是静态类的语义可以在 MSIL 中表示为“你不能创建它的实例”的唯一方法,你不能从它继承'以及'类本身不包含任何实例成员'是静态的合同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-03
  • 1970-01-01
  • 2012-05-19
相关资源
最近更新 更多