The easy one first. The reason that the compiler doesn't turn string.Format("{0}", myVal)
into string.Format{"{0}", myVal.ToString())
, is that there's no reason why it should. Should it turn BlahFooBlahBlah(myVal)
into BlahFooBlahBlah(myVal.ToString())
? Maybe that'll have the same effect but for better performance, but chances are it'll introduce a bug. Bad compiler! No biscuit!
Unless something can be reasoned about from general principles, the compiler should leave alone.
Now for the interesting bit IMO: Why does the former cause boxing and the latter not.
For the former, since the only matching signature is string.Format(string, object)
the integer has to be turned into an object (boxed) to be passed to the method, which expects to receive a string and an object.
The other half of this though, is why doesn't myVal.ToString()
box too?
When the compiler comes to this bit of code it has the following knowledge:
- myVal is an Int32.
- ToString() is defined by Int32
- Int32 is a value-type and therefore:
- myVal cannot possibly be a null reference* and:
- There cannot possibly be a more derived override - Int32.ToString() is effectively sealed.
Now, generally the C# compiler uses callvirt
for all method calls for two reasons. The first is that sometimes you do want it to be a virtual call after all. The second is that (more controversially) they decided to ban any method call on a null reference, and callvirt
has a built-in test for that.
In this case though, neither of those apply. There can't be a more derived class that overrides Int32.ToString(), and myVal cannot be null. It can therefore introduce a call
to the ToString()
method that passes the Int32
without boxing.
This combination (value can't be null, method can't be overriden elsewhere) only comes up with reference types much less often, so the compiler can't take as much advantage of it then (it also wouldn't cost as much, since they wouldn't have to be boxed).
This isn't the case if Int32
inherits a method implementaiton. For instance myVal.GetType()
would box myVal
as there is no Int32
override - there can't be, it's not virtual - so it can only be accessed by treating myVal
as an object, by boxing it.
The fact that this means that the C# compiler will use callvirt
for non-virtual methods and sometimes call
for virtual methods, is not without a degree of irony.
*Note that even a nullable integer set to null is not the same as a null reference in this regard.
string.Format("My value is {0}, myVal)
it's calling a formstring.Format(string, object)
rather thanstring.Format(string, object[])
. This has no bearing on the question, but it's worth noting that it has no bearing on the question - it applies across several similar calls. – Hankins{0:n}
, thenmyVal
shows1,234.00
andmyVal.ToString()
shows1234
. It's not the compiler's job to know what the resulting format will be. – Taproom