Since the value is boxed, all the compiler knows about is object
, so it is a regular virtual call to object.ToString()
, which will then pick up the overridden ToString()
of the struct. So: it is object.ToString()
that is invoked, and the Int32.ToString()
override which is executed.
private static void Main()
{
int x = 100;
object y = (object)x;
Console.Write(y.ToString());
}
becomes (comments mine):
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] int32 x,
[1] object y)
// int x = 100;
L_0000: ldc.i4.s 100
L_0002: stloc.0
// object y = (object)x;
L_0003: ldloc.0
L_0004: box int32
L_0009: stloc.1
// Console.Write(y.ToString());
L_000a: ldloc.1
L_000b: callvirt instance string [mscorlib]System.Object::ToString()
L_0010: call void [mscorlib]System.Console::Write(string)
L_0015: ret
}
The important line is at L_000b
; a regular virtual-call to object.ToString()
.
What gets more interesting is non-boxed value-types; if a value-type is known to have a ToString()
, then it can emit a static call:
private static void Main()
{
int x = 100;
Console.Write(x.ToString());
}
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] int32 x)
L_0000: ldc.i4.s 100
L_0002: stloc.0
L_0003: ldloca.s x
L_0005: call instance string [mscorlib]System.Int32::ToString()
L_000a: call void [mscorlib]System.Console::Write(string)
L_000f: ret
}
See the static-call (call
) at L_0005
. HOWEVER, in most value-type cases it will use a constrained call, which will be interpreted by the JIT as a static-call if it is overridden, and a virtual-call if it isn't:
private static void Main()
{
var x = new KeyValuePair<int, string>(123, "abc");
Console.Write(x.ToString());
}
becomes:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 3
.locals init (
[0] valuetype [mscorlib]System.Collections.Generic.KeyValuePair`2<int32, string> x)
L_0000: ldloca.s x
L_0002: ldc.i4.s 0x7b
L_0004: ldstr "abc"
L_0009: call instance void [mscorlib]System.Collections.Generic.KeyValuePair`2<int32, string>::.ctor(!0, !1)
L_000e: ldloca.s x
L_0010: constrained [mscorlib]System.Collections.Generic.KeyValuePair`2<int32, string>
L_0016: callvirt instance string [mscorlib]System.Object::ToString()
L_001b: call void [mscorlib]System.Console::Write(string)
L_0020: ret
}
The "constrained" / "callvirt" pair at L_0010
and L_0016
together make this construct, so it might not actually be a virtual call. The JIT / runtime can do other voodoo on it. This is discussed more here.
Note that a regular class
will always use virtual call for this, except for the scenario return base.ToString();
, which is a static-call to the base-types implementation.