简体   繁体   中英

What does “final” mean in IL?

When using ildasm/ilasm, you can observe the MSIL/CIL code produced by a compiler (the C# compiler, for example), and in some cases you can see that there are methods marked as virtual final .

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.

But how? How does it work, in practice?

Is it just enforced by the compiler (eg if you try to override a sealed method, it will fail with a compilation error) or by the CLR (eg when you try the overriding method, the CLR throws), or by both? Is there any legal way of "removing", circumventing, this final , ie to write a derived class that overrides this method?

Update : I probably have found a way (see my own answer) but I am not sure at all this is legal, supported, standard... so I have posted another question on this new (but related) problem here

I am open to comments and, of course, alternative ways (if they exist!) to do it.

This is for methods which are coming from some interface. It is an implementation detail. If a class implements some interface, then the method from interface is marked as virtual , that is for polymorphic behaviour (a place in virtual table (v table) ). But it is also marked as final (Sealed) so that the further child classes implementing the base class can't override that particular method.

Consider the following example:

interface SomeInterface
{
    void SomeMethod();
}

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

From: 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.

Yes. It is as such the equivalent of the C# keyword sealed so for example:

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

Becomes:

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

virtual relates to the C# keyword virtual but also to any other virtual method. This includes override and also any interface implementation. In particular while you can't define a method as virtual sealed in C# (because it would be pointless, so you should decide just what you want to do) you can in CIL, and this is done in some interface implementations.

To the C# perspective there are three ways to implement an interface method or property:

  1. Non-virtual.
  2. Virtual (so derived classes can override it).
  3. Explicit implementation.

To the CIL perspective all of those are virtual as they all use the virtual-method mechanism. The first and third are also final .

(Consider that we can ignore the over-ride mechanism and use call in CIL to call a method as it is defined in a class, but we have to use callvirt with an interface because we don't know what class we are calling on, so there has to be a lookup).

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

Both.

The C# compiler will not allow you to override a sealed method:

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

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

This refuses to compile with compiler error CS0239: "'Test1.ToString()' : cannot override inherited member 'Test.ToString()' because it is sealed".

But if you force it by writing the CIL yourself:

.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
  }
}

Then if you call new Test().ToString() it returns "a" as you'd expect, but if you call new Test1().ToString() you get a runtime error:

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'.

Indeed, this makes the entire class an unloadable type; it's new Test1() that throws, not the ToString() .

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

No. The TypeLoadException will happen with any class that attempts to override a final method. Your only option is to edit the base class and compile it again.

I'll try to answer my own question with a couple of experiments I did, since I wanted to really understand how it works in practice (yes, even if it is an implementation detail).

I wrote a class in IL, named C2, which extends a C# class named C1. The method it overrides, M1, is written in IL as virtual final

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

This shows up in ILSpy as

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

Visual Studio and CSC see it correctly as M1(), and will call it correctly (tried in the debugger).

Now, this is what my question was about: this should be the last method in the chain, but who says it?

I wrote a derived class C3, which tries to override the method again.

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

It assembles with ILasm, and it produces a DLL. In ILSpy:

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

I used it in a C# project, and it compiles, but when you try to instantiate 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`

So it seems this is enforced by the C# compiler, not by ILasm (it is possible to override a final method), but it is then checked by the CLR.

The final part of my question: can you work around it?

It seems there is a way. In IL, you can override a method with a different name. So, we change the name (M1_2() overrides M1()), say it overrides the method on the base class (C1::M2()), a la explicit interface implementation, and the "final" on the intermediate (C2) class does not matter anymore.

.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 assembles, and it shows in ILSpy as

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

Then in the same class, you can define a new M1 which calls M1_2().

If you call

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

then M1_2 is called correctly. It seems that the CLR enforces the constraint only if the chain is direct ( C1::M1 > C2::M1 > C3::M1 ) and not if you do "jump" over the hiearchy ( C1::M1 > C3::M1_2 ). You have to choose a different name, though. If you use the same name (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

will not work, throwing the same exception.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM