According to boxing on structs when calling ToString(), calling an unoverridden method on a C# struct
causes it to be boxed. This is ultimately because of how the constrained callvirt
IL instruction works, which, according to the documentation, says that:
If
thisType
is a value type andthisType
does not implementmethod
thenptr
is dereferenced, boxed, and passed as the 'this' pointer to thecallvirt
method instruction.
However, it doesn't explain why that is the way it is, especially if the struct
does implement the method, it doesn't box the struct
:
If
thisType
is a value type andthisType
implementsmethod
thenptr
is passed unmodified as the 'this' pointer to acall
method
instruction, for the implementation ofmethod
bythisType
.
Why can't the runtime just do the same even if the method wasn't implemented or overridden by the struct
? This probably has something to do with how the runtime calls methods, but I can't figure it out.
Simply, the method object.ToString()
is written under the assumption that this
will be an instance of object
. (That is, after all, how instance methods work). You can't pass it a pointer to a struct and expect it to work.
All reference types have an object header and method table. However, structs do not (there's no need). Therefore if you just pass a (pointer to a) struct to a method which is expecting a pointer to an object
, that method will probably crash in exciting and fatal ways.
When you box a struct, it gets copied to the heap, and it's given an object header and method table. This is why a boxed struct is a valid object
. Therefore, the way to call a method which expects an object
, but pass a struct, is to box the struct.
When you define an override of ToString
on the struct itself, that method expects a pointer to that struct (and doesn't assume that the pointer points to a method table), so you can directly pass a struct without boxing.
(Actually, when you override ToString
in a struct, the runtime emits two methods. One is the method you defined which expects this
to be a pointer to a struct, and the other is a "thunk" which expects this
to be an object
(a boxed struct), which tweaks the this
pointer to point to the struct itself rather than the method table, then calls the first method. This lets you call ToString
on a boxed struct. I won't repeat the gory details, but see this excellent article by Matt Warren).