"final" 在 IL 中是什么意思?

What does "final" mean in IL?

当使用 ildasm/ilasm 时,您可以观察编译器(例如 C# 编译器)生成的 MSIL/CIL 代码,并且在某些情况下您可以看到标记为 virtual final.

在这种情况下,final 是什么意思?我的猜测是它的意思是 "this method cannot be overridden",所以即使这个方法在虚拟 table 中有一个插槽,该插槽也不能被派生 class 中的方法覆盖。

但是怎么办?它在实践中是如何工作的?

它是由编译器强制执行的(例如,如果您尝试覆盖密封方法,它将因编译错误而失败)还是由 CLR(例如,当您尝试覆盖方法时,CLR 抛出),或者由两者? "removing" 是否有任何合法的方法绕过此 final,即编写一个派生的 class 来覆盖此方法?

更新:我可能找到了一种方法(参见我自己的回答)但我完全不确定这是合法的、受支持的、标准的……所以我发布了关于这个新的(但相关的)问题的另一个问题

我愿意接受评论,当然还有其他方法(如果存在的话!)。

这是针对来自某些接口的方法。它是一个实现细节。如果一个 class 实现了一些接口,那么接口中的方法被标记为 virtual,这是为了多态行为(虚拟 table (v table)).但它也被标记为 final (Sealed) 以便实现基础 class 的进一步子 classes 不能覆盖该特定方法.

考虑以下示例:

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)

The C# compiler requires that a method that implements an interface method signature be marked as public. The CLR requires that interface method be marked as virtual. If you do not explicitly mark the method as virtual in your source code, the compiler marks the method as virtual and sealed; this prevents a derived class from overriding the interface method. If you explicitly mark the method as virtual, the compiler marks the method as virtual (and leaves it unsealed); this allows a derived class to override the interface method.

What does final mean in this context? My guess is that it means "this method cannot be overridden", so even if this method has a slot in the virtual table, that slot cannot be overwritten by a method in a derived class.

是的。它本身等同于 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. 虚拟(因此派生的 classes 可以覆盖它)。
  3. 显式实现。

从 CIL 的角度来看,所有这些都是 virtual,因为它们都使用虚拟方法机制。第一第三也是final.

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

Is it just enforced by the compiler (e.g. if you try to override a sealed method, it will fail with a compilation error) or by the CLR (e.g. when you try the overriding method, the CLR throws), or by both?

两者都有。

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() 它 returns "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'.

确实,这使得整个 class 成为可卸载类型;抛出的是 new Test1(),而不是 ToString().

Is there any legal way of "removing", circumventing, this final, i.e. to write a derived class that overrides this method?

没有。 TypeLoadException 将发生在任何试图覆盖 final 方法的 class 中。您唯一的选择是编辑基础 class 并再次编译它。

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

我在 IL 中写了一个 class,名为 C2,它扩展了一个名为 C1 的 C# class。它覆盖的方法 M1 在 IL 中被写为 virtual final

.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(),并将正确调用它(在调试器中尝试)。

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

我写了一个派生的class 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 时:

An unhandled exception of type 'System.TypeLoadException' occurred in mscorlib.dll Additional information: Declaration referenced in a method implementation cannot be a final method`

所以这似乎是由 C# 编译器强制执行的,而不是由 ILasm 强制执行的(可以覆盖 final 方法),但随后由 CLR 检查。

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

看来是有办法的。在 IL 中,您可以重写具有不同名称的方法。 因此,我们更改名称(M1_2() 覆盖 M1()),说它覆盖基 class (C1::M2()) 上的方法,a la显式接口实现,和中间(C2)上的"final"class无所谓了。

.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()

然后在同一个class中,你可以定义一个new M1调用M1_2()。

如果你打电话

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

然后 M1_2 被正确调用。似乎 CLR 仅在链是直接链 (C1::M1 > C2::M1 > C3::M1) 时才强制执行约束,如果您在层次结构上执行 "jump" (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

将不起作用,抛出相同的异常。