How is it possible that a virtual method
System.Object.ToString
is called on a unboxed value type, which (according to section I.8.9.7 of the CLI specification) has no base type at all?
I'm confused by the question. What does having or not having a base type have anything to do with it?
I would like to know exactly what is going on in the three lines
The key is the constrained
prefix. The documentation -- Partition III section 2.1 -- is pretty straightforward. In the documentation we have a type of a receiver thisType
, a managed pointer to that type ptr
, and a constrained.callvirt
of method
. The rules are:
- If
thisType
is a reference type thenptr
is dereferenced and passed as thethis
pointer to thecallvirt
ofmethod
- If
thisType
is a value type andthisType
implementsmethod
thenptr
is passed unmodified as thethis
pointer to acall
ofmethod
implemented bythisType
- If
thisType
is a value type andthisType
does not implement method thenptr
is dereferenced, boxed, and passed as thethis
pointer to thecallvirt
ofmethod
In your example point (3) applies. The type Foo
is a value type, it does not implement the method ToString
, so it is boxed and the method (provided by a base class) is called with the reference to the box as this
.
Suppose we had int.ToString. Then point (2) applies. The type is int
, it is a value type, and int
implements an override of System.Object.ToString()
. So the managed pointer to the int
becomes the this
of the call to ToString
. The unnecessary boxing is thereby elided. (And if ToString
mutated the int
then the mutation would happen on the variable given as the receiver, not on the boxed copy.)
Why can a virtual method be called on a unboxed value that does not inherit that virtual method?
The question at hand is whether or not the method is implemented, as is called out in the documentation I quoted above. What does inheritance have to do with it?
A question you did not ask but this is a nice place to answer it:
Should I always implement ToString on my value types?
Well, I don't know about always, but it certainly is a good idea to do so because (1) the default implementation of ToString
is dismal, and (2) by implementing it on your value types you can manage to elide the boxing penalty any time the method is called directly.
Does the same go for other virtual methods of object?
Yep. And there are good reasons to make your own equality and hashing in value types anyways. The default value type equality can sometimes be unexpected.
I note that GetType is not virtual. Is that relevant?
Yes; not being virtual means it cannot be overridden in a value type, which means that calling GetType
on any value type always boxes it. Of course, if you have an unboxed value type in hand then you don't need to call GetType
because you already know what its type is at compile time!