When delegate
keyword is used in C#, the C# compiler automatically generates a class derived from System.MulticastDelegate
class.
This compiler generated class contains 3 methods as well: Invoke, BeginInvoke and EndInvoke
.
All these three methods are marked public virtual extern
but interestingly the class itself is marked sealed
.
Virtual methods defined in a sealed class not only strikes as counter-intuitive but are actually illegal in C#.
So my question is, is there a specific reason for this or is it just one of those harmless things done keeping in mind some hypothetical future enhancement?
Edit 1:
Can the reason be to force use of 'callVirt' IL opcode as opposed to 'call' so that delegate object is always checked for null by the CLR before trying to execute any of the three methods? Though I fail to see why a delegate
should be a special case in this respect.
Also isn't it a performance hit to force use of callvirt
(though it may be minuscule)
Edit 2:
Added CIL tag, as it turns out that the C# way of defining delegates is in fact mandated by the CIL standard. Standard states that (following is not full text)
Delegates shall have a base type of System.Delegate. Delegates shall be declared sealed, and the only members a delegate shall have are either the first two or all four methods as specified here. These methods shall be declared runtime and managed. They shall not have a body, since that body shall be created automatically by the VES. Other methods available on delegates are inherited from the class System.Delegate in the Base Class Library. The delegate methods are:
- The instance constructor
- The Invoke method shall be virtual
- The BeginInvoke method, if present, shall be virtual
- The EndInvoke method shall be virtual
So this is definitely not a side effect of compiler process or is similar to other interesting compiler outputs.
If standard emphasizes something, it must be for some good reason and rationale.
So the question now is why does CIL standard for delegates emphasizes on sealed and virtual at the same time?
Does the catch lie here?:
They shall not have a body, since that body shall be created automatically by the VES.
Are they marked virtual so that the VES/CLR generated body can be executed on invocation of these methods?
A sealed class cannot be used as a base class, and a virtual method has to be implemented in a derived class, which is a contradiction.
Sealed classes are used to restrict the users from inheriting the class. A class can be sealed by using the sealed keyword. The keyword tells the compiler that the class is sealed, and therefore, cannot be extended. No class can be derived from a sealed class.
you can create instance of sealed class but not in static class. You need the class name to access the members and methods of static class whereas in case of sealed class you can create the instance of it.
A sealed class, in C#, is a class that cannot be inherited by any class but can be instantiated. The design intent of a sealed class is to indicate that the class is specialized and there is no need to extend it to provide any additional functionality through inheritance to override its behavior.
You are being tripped up by the disassembler you used to look at the type definition. Which must translate the IL back to a recognizable language, like C#. This is not in general possible to do with full fidelity, the rules for IL are not the same as the C# language rules. This doesn't just happen for delegates, an interface implementation method is virtual as well, even though you don't declare it virtual in your C# code.
To further muddy the waters, IL actually permits a compiler to emit a non-virtual call for a virtual method if it can determine the target object from code analysis. But that will never happen for a delegate or interface call. And IL permits making a virtual call to a non-virtual method, something the C# compiler does with gusto to implement the guarantee that an instance method can never be called with a null this.
But that C# usage is a clever trick, discovered only after the CLR was designed. The original intent of virtual certainly was to annotate that the method should be called with Callvirt. Ultimately it doesn't matter because the compiler is aware of delegate and interface behavior and will always emit Callvirt. And the actual method call is implemented in CLR code which assumes a Callvirt activation.
As I noted in my question that this sealed virtual anomaly is in fact mandated by CIL standard. It remains unclear why CIL standard specifically mentions that delegate methods Invoke
, BeginInvoke
and EndInvoke
should be virtual while at the same time mandating to seal the Delegate
inherited class.
Also, after going through SSCLI code I learnt that internal optimization of JIT compiler automatically translates any callvirt
call on a virtual method of a sealed class to normal call with additional null check. This means that delegates do not suffer any performance hit when its Invoke (or any of the other two) method is called through callvirt
instruction despite being marked virtual in the IL.
When a delegate's invoke is called, CLR automatically emits a highly optimized body for this method as opposed to compiling IL code to generate body which it does for 'normal' methods. This has nothing to do with being marked virtual
in the IL.
I have also verified by hand modifying IL code and re-assembling it that virtual can be safely removed from the generated delegate class's IL code. The generated assembly despite being in violation of the CIL standard runs perfectly fine.
.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
Note that I have converted the virtual methods to normal instance methods.
Since this changed IL runs perfectly fine it proves that standard mandated virtual methods in the sealed delegate class are not necessary. They can be normal instance methods as well.
So in all probability this anomaly is either to emphasize that calling these three delegate methods will in-fact result in calling of some other method (i.e. run-time polymorphism just like 'normal' virtual methods) or this has been so to accommodate some future hypothetical enhancement related to delegates.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With