什么"最终" IL的意思是?

时间:2015-11-26 17:17:13

标签: c# override clr cil

使用ildasm / ilasm时,您可以观察编译器(例如C#编译器)生成的MSIL / CIL代码,在某些情况下,您可以看到有标记为"\0"的方法。 / p>

virtual final在这种情况下的含义是什么?我的猜测是它意味着"这个方法不能被覆盖",所以即使这个方法在虚拟表中有一个槽,该槽也不能被派生类中的方法覆盖。

但是怎么样?它在实践中如何运作?

它是否仅由编译器强制执行(例如,如果您尝试覆盖密封方法,它将因编译错误而失败)或CLR(例如,当您尝试覆盖方法时,CLR抛出),或者两者都执行? 有没有合法的方式去除"绕过这个final,即写一个覆盖这个方法的派生类?

更新:我可能找到了一种方法(请参阅我自己的答案),但我不确定这是合法的,支持的,标准的......所以我在这个新的帖子上发布了另一个问题(但相关)问题here

我愿意接受评论,当然还有其他方式(如果它们存在!)来做这件事。

3 个答案:

答案 0 :(得分:5)

这适用于来自某个界面的方法。这是一个实现细节。如果一个类实现了某个接口,则接口的方法标记为virtual,即多态行为(虚拟表中的位置(v table))。但它也标记为final (密封),以便实现基类的其他子类不能覆盖该特定方法。

考虑以下示例:

interface SomeInterface
{
    void SomeMethod();
}

class SomeClass : SomeInterface
{
    public void SomeMethod() //This will be marked as virtual final in IL
    {
        //anything
    }
}

来自:CLR via C# (Jeffrey Richter)

  

C#编译器需要一个实现接口的方法   方法签名被标记为公共。 CLR需要这样做   接口方法标记为虚拟。如果您没有明确标记   在源代码中作为虚拟方法,编译器标记出来   方法为虚拟和密封; 这可以防止派生类   覆盖接口方法。如果您明确将方法标记为   虚拟,编译器将方法标记为虚拟(并留下它   启封);这允许派生类覆盖接口   方法

答案 1 :(得分:3)

  

在这种情况下,最终意味着什么?我的猜测是它意味着“此方法无法被覆盖”,因此即使此方法在虚拟表中有一个插槽,该插槽也不能被派生类中的方法覆盖。

是。它就是C#关键字sealed的等价物,例如:

public override sealed string ToString()
{
  return "";
}

变为:

.method public final hidebysig virtual instance string ToString () cil managed 
{
  ldstr ""
  ret
}

virtual与C#关键字virtual相关,但与任何其他虚拟方法相关。这包括override以及任何接口实现。特别是当您无法在C#中将方法定义为virtual sealed时(因为它没有意义,因此您应该决定您想要做什么),您可以在CIL中完成,这在一些接口实现中完成。

对于C#透视图,有三种方法可以实现接口方法或属性:

  1. 非虚拟的。
  2. 虚拟(因此派生类可以覆盖它)。
  3. 明确实施。
  4. 对于CIL透视图,所有这些都是virtual,因为它们都使用虚方法机制。第一个和第三个也是final

    (考虑到我们可以忽略覆盖机制并在CIL中使用call来调用类中定义的方法,但是我们必须使用带有接口的callvirt因为我们不知道我们要求什么类,所以必须有一个查找)。

      

    它是否仅由编译器强制执行(例如,如果您尝试覆盖密封方法,它将因编译错误而失败)或CLR(例如,当您尝试覆盖方法时,CLR抛出),或者两者都执行?

    两个

    C#编译器不允许您覆盖密封方法:

    public class Test
    {
      public override sealed string ToString()
      {
        return "a";
      }
    }
    
    public class Test1 : Test
    {
      public override string ToString()
      {
        return "";
      }
    }
    

    这拒绝使用编译器错误CS0239进行编译:“'Test1.ToString()':无法覆盖继承的成员'Test.ToString()',因为它已被密封”。

    但如果你自己写CIL强迫它:

    .class public auto ansi beforefieldinit Test extends [mscorlib]System.Object
    {
      .method public hidebysig specialname rtspecialname instance void .ctor () cil managed 
      {
        ldarg.0
        call instance void [mscorlib]System.Object::.ctor()
        ret
      }
    
      .method public final hidebysig virtual instance string ToString () cil managed 
      {
        ldstr "a"
        ret
      }
    }
    
    .class public auto ansi beforefieldinit Test2 extends Mnemosyne.Test
    {
      .method public hidebysig specialname rtspecialname instance void .ctor () cil managed 
      {
        IL_0000: ldarg.0
        IL_0001: call instance void Mnemosyne.Test::.ctor()
        IL_0006: ret
      }
    
      .method public hidebysig virtual instance string ToString () cil managed 
      {
        ldstr ""
        ret
      }
    }
    

    然后,如果您调用new Test().ToString(),它会按预期返回"a",但如果您调用new Test1().ToString(),则会收到运行时错误:

    System.TypeLoadException : Declaration referenced in a method implementation cannot be a final method.  Type: 'TestAssembly.Test2'.  Assembly: 'TestAssembly, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null'.
    

    实际上,这使得整个班级成为无法加载的类型;抛出的是new Test1(),而不是ToString()

      

    是否有任何合法的方法可以“删除”,绕过这个最终版本,即编写一个覆盖此方法的派生类?

    没有。任何尝试覆盖TypeLoadException方法的类都会发生final。您唯一的选择是编辑基类并重新编译它。

答案 2 :(得分:1)

我会尝试用我做过的几个实验来回答我自己的问题,因为我想真正理解它在实践中是如何工作的 (是的,即使它是一个实现细节)。

我在IL中编写了一个名为C2的类,它扩展了一个名为C1的C#类。它覆盖的方法M1,在IL中写为虚拟最终

.method private hidebysig virtual final 
      instance object  M1() cil managed
{
   .override N.C1::M1

这在ILSpy中显示为

public class C2 : C1
{
    object M1()
    {
        ...
    }

Visual Studio和CSC将其正确地视为M1(),并将其正确调用(在调试器中尝试)。

现在,这就是我的问题所在:这应该是链中的最后一个方法,但是谁说的呢?

我写了一个派生类C3,它试图再次覆盖该方法。

.method private hidebysig virtual final 
      instance object  M1() cil managed
{
   .override N.C2::M1

它与ILasm组装,它产生一个DLL。 在ILSpy:

public class C3 : C2
{
    object M1()
    {
        ...
    }

我在C#项目中使用它,它编译,但是当你尝试实例化C1:

  

mscorlib.dll中发生了未处理的“System.TypeLoadException”类型异常   附加信息:方法实现中引用的声明不能是最终方法。

所以看起来这是由C#编译器强制执行的,而不是由ILasm强制执行(可以覆盖最终方法),但它会被CLR检查。

我的问题的最后一部分:你可以解决它吗?

似乎有办法。在IL中,您可以使用其他名称覆盖方法。 所以,我们更改名称(M1_2()覆盖M1()),说它覆盖基类上的方法(C1 :: M2()), a la 显式接口实现,以及“最后“在中级(C2)课上不再重要了。

.class public auto ansi beforefieldinit N.C3
   extends N.C2
{ 
   .method private hidebysig virtual final 
      instance object  M1_2() cil managed
   {
      .override N.C1::M1

Il组装,它在ILSpy中显示为

public class C3 : C2
{
    object C1.M1_2()

然后在同一个班级中,您可以定义一个调用M1_2()的new M1。

如果你打电话

C1 obj = new C3();
obj.M1();

然后正确调用M1_2。似乎CLR仅在链是直接(C1::M1 > C2::M1 > C3::M1)时强制执行约束,而不是如果你“跳过”hiearchy(C1::M1 > C3::M1_2)则不强制执行约束。但是,您必须选择其他名称。如果您使用相同的名称(M1):

.class public auto ansi beforefieldinit N.C3
   extends N.C2
{ 
   .method private hidebysig virtual final 
      instance object  M1() cil managed
   {
      .override N.C1::M1

无效,抛出相同的异常。