Usually, treating a struct S
as an interface I
will trigger autoboxing of the struct, which can have impacts on performance if done often. However, if I write a generic method taking a type parameter T : I
and call it with an S
, then will the compiler omit the boxing, since it knows the type S
and does not have to use the interface?
This code shows my point:
interface I{
void foo();
}
struct S : I {
public void foo() { /* do something */ }
}
class Y {
void doFoo(I i){
i.foo();
}
void doFooGeneric<T>(T t) where T : I {
t.foo(); // <--- Will an S be boxed here??
}
public static void Main(string[] args){
S x;
doFoo(x); // x is boxed
doFooGeneric(x); // x is not boxed, at least not here, right?
}
}
The doFoo
method calls foo()
on an object of type I
, so once we call it with an S
, that S
will get boxed. The doFooGeneric
method does the same thing. However, once we call it with an S
, no autoboxing might be required, since the runtime knows how to call foo()
on an S
. But will this be done? Or will the runtime blindly box S
to an I
to call the interface method?
void doFooGeneric<T>(T t) where T : I {
t.foo(); // <--- Will an S be boxed here??
}
Boxing will be avoided there!
The struct type S
is sealed. For value type versions of the type parameter T
to your method doFooGeneric
above, the C# compiler gives code that calls the relevant struct member directly, without boxing.
Which is cool.
See Sameer's answer for some technical details.
OK, so I came up with an example of this. I will be interested in better examples if anyone has some:
using System;
using System.Collections.Generic;
namespace AvoidBoxing
{
static class Program
{
static void Main()
{
var myStruct = new List<int> { 10, 20, 30, }.GetEnumerator();
myStruct.MoveNext(); // moves to '10' in list
//
// UNCOMMENT ONLY *ONE* OF THESE CALLS:
//
//UseMyStruct(ref myStruct);
//UseMyStructAndBox(ref myStruct);
Console.WriteLine("After call, current is now: " + myStruct.Current); // 10 or 20?
}
static void UseMyStruct<T>(ref T myStruct) where T : IEnumerator<int>
{
myStruct.MoveNext();
}
static void UseMyStructAndBox<T>(ref T myStruct)
{
((IEnumerator<int>)myStruct).MoveNext();
}
}
}
Here the type of myStruct
is a mutable value-type which holds a reference back to the List<>
, and also holds "counter" that remembers what index in the List<>
we have reached until now.
I had to use ref
, otherwise the value-type would be copied by value when passed into either of the methods!
When I uncomment the call to UseMyStruct
(only), this method moves the "counter" inside our value type one position ahead. If it did that in a boxed copy of the value type, we would not see it in the original instance of the struct.
To see what the difference is with boxing, try the call UseMyStructAndBox
instead (comment UseMyStruct
again). It creates a box at the cast, and the MoveNext
happens on a copy. So the output is different!
To those who are unhappy with (or confused by) the ref
, just write out Current
from within the method instead. Then we can get rid of ref
. Example:
static void F<T>(T t) where T : IEnumerator<int>
{
t.MoveNext(); // OK, not boxed
Console.WriteLine(t.Current);
}
static void G<T>(T t) where T : IEnumerator<int>
{
((IEnumerator<int>)t).MoveNext(); // We said "Box!", it will box; 'Move' happens to a copy
Console.WriteLine(t.Current);
}
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